Bài 30.3: Image Optimization với next/image

Chào mừng trở lại với series blog về Lập trình Web Front-end!

Trong thế giới web hiện đại, hình ảnh đóng vai trò cực kỳ quan trọng. Chúng giúp truyền tải thông điệp, thu hút người dùng và làm cho trang web trở nên sống động hơn. Tuy nhiên, hình ảnh cũng là một trong những nguyên nhân chính khiến trang web của bạn bị chậm đi, ảnh hưởng tiêu cực đến trải nghiệm người dùng và thậm chí là thứ hạng SEO. Việc tối ưu hóa hình ảnh là không thể bỏ qua!

Nếu bạn đang làm việc với Next.js, bạn thật may mắn! Framework này cung cấp một giải pháp cực kỳ mạnh mẽ và tiện lợi để giải quyết bài toán tối ưu hóa hình ảnh một cách tự động và hiệu quả: đó chính là component Image từ next/image.

Hãy cùng đi sâu vào sức mạnh của nó nhé!

Vấn đề với thẻ <img> truyền thống

Trước khi có next/image, chúng ta thường sử dụng thẻ <img> đơn giản như thế này:

<img src="/path/to/my-large-image.jpg" alt="Mô tả hình ảnh">

Có vẻ đơn giản, đúng không? Nhưng cách tiếp cận này lại ẩn chứa nhiều vấn đề tiềm ẩn:

  1. Không tự động tối ưu hóa: Trình duyệt sẽ tải nguyên file ảnh gốc, bất kể kích thước hay định dạng của nó. Nếu ảnh quá lớn, tốc độ tải trang sẽ rất chậm.
  2. Không phù hợp với các thiết bị khác nhau: Cùng một ảnh được tải về cho cả màn hình điện thoại nhỏ và màn hình máy tính lớn, lãng phí băng thông cho người dùng di động.
  3. Không có Lazy Loading mặc định: Tất cả ảnh trên trang (kể cả những ảnh nằm ở cuối trang, ngoài tầm nhìn của người dùng) đều có thể bắt đầu tải ngay khi trang được render, làm chậm quá trình hiển thị nội dung chính.
  4. Gây ra Layout Shift: Khi ảnh được tải về sau, nó có thể đẩy các nội dung khác xuống dưới, gây ra hiện tượng nhảy layout (Cumulative Layout Shift - CLS) cực kỳ khó chịu cho người dùng và ảnh hưởng đến điểm Core Web Vitals của trang.
  5. Không hỗ trợ các định dạng ảnh hiện đại: Khó khăn trong việc tự động chuyển đổi sang các định dạng tối ưu hơn như WebP hay AVIF để giảm kích thước file mà vẫn giữ nguyên chất lượng.

Đây chính là lúc next/image xuất hiện như một vị cứu tinh!

Giới thiệu next/image

next/image là một component React được xây dựng sẵn trong Next.js, được thiết kế đặc biệt để tối ưu hóa việc hiển thị hình ảnh trên website của bạn. Nó không chỉ thay thế thẻ <img> mà còn mang lại một loạt các tính năng tuyệt vời:

  • Tự động tối ưu hóa kích thước và định dạng: Next.js sẽ tự động tạo ra nhiều phiên bản ảnh với các kích thước và định dạng khác nhau (thường là WebP hoặc AVIF nếu trình duyệt hỗ trợ) ngay khi build hoặc theo yêu cầu. Nó sẽ chỉ phục vụ phiên bản phù hợp nhất với thiết bị và trình duyệt của người dùng.
  • Lazy Loading mặc định: Theo mặc định, ảnh sẽ chỉ được tải khi người dùng cuộn đến gần vị trí của nó trên màn hình, tiết kiệm băng thông và tăng tốc độ hiển thị nội dung ban đầu.
  • Ngăn chặn Layout Shift: Bằng cách yêu cầu bạn cung cấp kích thước ảnh (widthheight), component Image sẽ dành sẵn không gian cho ảnh trước khi nó được tải, đảm bảo nội dung xung quanh không bị xê dịch.
  • Hỗ trợ linh hoạt: Hoạt động tốt với cả ảnh tĩnh trong thư mục public và ảnh từ các nguồn bên ngoài (CDN, server khác).
  • Tương thích với các định dạng ảnh hiện đại: Dễ dàng sử dụng các định dạng ảnh mới nhất để cải thiện hiệu suất.

Nghe thật tuyệt vời phải không? Hãy xem cách sử dụng nó như thế nào.

Cách sử dụng next/image cơ bản

Để sử dụng component Image, bạn cần import nó từ next/image:

import Image from 'next/image';

Sau đó, bạn chỉ cần thay thế thẻ <img> bằng <Image> và truyền các props cần thiết. Hai props bắt buộcsrcalt, cùng với widthheight (hoặc sử dụng prop fill trong trường hợp đặc biệt).

Ví dụ cơ bản với ảnh tĩnh

Giả sử bạn có một ảnh logo.png trong thư mục public/images.

import Image from 'next/image';

function MyComponent() {
  return (
    <div>
      <h1>Logo của tôi</h1>
      <Image
        src="/images/logo.png" // Đường dẫn từ thư mục public
        alt="Logo của công ty tôi" // Mô tả ảnh cho accessibility
        width={150} // Chiều rộng gốc của ảnh (px)
        height={50} // Chiều cao gốc của ảnh (px)
      />
      <p>Chào mừng đến với website của chúng tôi!</p>
    </div>
  );
}

Giải thích:

  • import Image from 'next/image';: Dòng này mang component Image vào để sử dụng.
  • src="/images/logo.png": Chỉ định đường dẫn đến file ảnh. Đối với ảnh trong thư mục public, đường dẫn bắt đầu từ gốc của thư mục public.
  • alt="Logo của công ty tôi": Prop bắt buộc, cung cấp mô tả cho ảnh, quan trọng cho SEO và người dùng sử dụng trình đọc màn hình.
  • width={150}height={50}: Đây là kích thước gốc của ảnh hoặc kích thước bạn muốn dành chỗ cho ảnh trên layout. Next.js sử dụng thông tin này để tính toán tỷ lệ khung hình, ngăn chặn layout shift và tạo ra các phiên bản ảnh tối ưu với kích thước phù hợp. Việc cung cấp widthheight (hoặc sử dụng fill) là cực kỳ quan trọng để tận dụng hết sức mạnh của next/image.

Khi component này được render, Next.js sẽ không chỉ đưa thẻ <img> đơn giản vào HTML. Thay vào đó, nó sẽ tạo ra một cấu trúc phức tạp hơn, có thể bao gồm cả srcset để trình duyệt chọn ảnh tối ưu nhất dựa trên kích thước màn hình và độ phân giải, đồng thời thiết lập các kỹ thuật lazy loading và dành sẵn không gian trên layout.

Xử lý ảnh từ nguồn bên ngoài (Remote Images)

Nếu bạn lưu trữ ảnh trên một server khác, một CDN (Content Delivery Network) hoặc các dịch vụ như Firebase Storage, S3,... bạn cần cấu hình cho Next.js biết để nó có thể tối ưu hóa các ảnh này.

Bạn cần khai báo các domain chứa ảnh trong file next.config.js ở thư mục gốc của project.

// next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true, // Cài đặt mặc định cho Next.js project mới
  images: {
    // Phương pháp domains (phù hợp với Next.js cũ hơn hoặc cấu hình đơn giản)
    // domains: ['example.com', 'another-cdn.com'],

    // Phương pháp remotePatterns (ưu tiên dùng trong Next.js 13+)
    remotePatterns: [
      {
        protocol: 'https', // Giao thức (http hoặc https)
        hostname: 'cdn.myremoteserver.com', // Tên miền chứa ảnh
        port: '', // Cổng (thường để trống cho http/s)
        pathname: '/path/to/images/**', // Đường dẫn cụ thể nếu cần giới hạn
      },
      {
        protocol: 'https',
        hostname: 'firebasestorage.googleapis.com',
        // Không cần pathname nếu muốn chấp nhận tất cả ảnh từ domain này
      },
    ],
  },
};

module.exports = nextConfig;

Giải thích:

  • Bạn thêm một object images vào trong nextConfig.
  • Bên trong images, bạn có thể dùng domains (là một mảng các chuỗi tên miền) hoặc remotePatterns (là một mảng các object chi tiết hơn). remotePatterns được khuyến khích sử dụng trong các phiên bản Next.js mới hơn vì nó cho phép cấu hình chi tiết và bảo mật hơn.
  • Bạn liệt kê các domain (hoặc pattern) mà ứng dụng của bạn sẽ tải ảnh từ đó. Next.js cần biết điều này để thực hiện việc tối ưu hóa.
  • Sau khi thay đổi next.config.js, bạn cần khởi động lại server dev hoặc build lại project để cấu hình có hiệu lực.

Sau khi cấu hình xong, bạn có thể sử dụng component Image với đường dẫn đầy đủ đến ảnh từ nguồn bên ngoài:

import Image from 'next/image';

function RemoteImageExample() {
  const imageUrl = "https://cdn.myremoteserver.com/path/to/images/product-item-123.jpg";

  return (
    <div>
      <h2>Sản phẩm ABC</h2>
      <Image
        src={imageUrl}
        alt="Hình ảnh sản phẩm ABC"
        width={400} // Kích thước gốc hoặc dự kiến hiển thị
        height={300} // Kích thước gốc hoặc dự kiến hiển thị
      />
      <p>Đây là mô tả chi tiết về sản phẩm.</p>
    </div>
  );
}

Lưu ý: Dù là ảnh tĩnh hay ảnh từ nguồn ngoài, việc cung cấp widthheight là rất quan trọng cho hiệu suất và tránh layout shift.

Các Prop quan trọng khác

next/image cung cấp thêm nhiều props mạnh mẽ để bạn kiểm soát hành vi hiển thị của ảnh:

Prop fill (Thay thế layout="fill")

Trong các phiên bản Next.js mới, prop layout đã bị loại bỏ và thay bằng prop fill. Prop này rất hữu ích khi bạn muốn ảnh lấp đầy toàn bộ kích thước của phần tử cha mà không cần biết trước kích thước cụ thể của ảnh.

Khi sử dụng fill, bạn không cần cung cấp widthheight cho component Image. Tuy nhiên, phần tử cha chứa component Image phải có position khác với static (ví dụ: relative, absolute, fixed).

import Image from 'next/image';

function BackgroundImageContainer() {
  return (
    // Parent div cần có position: 'relative', 'absolute', hoặc 'fixed'
    <div style={{ position: 'relative', width: '100%', height: '500px' }}>
      <Image
        src="/images/hero-background.jpg"
        alt="Ảnh nền trang chủ"
        fill // Ảnh sẽ lấp đầy kích thước của parent div
        style={{ objectFit: 'cover' }} // Cách ảnh fit vào container (cover, contain, ...)
      />
      {/* Nội dung khác chồng lên ảnh nền */}
      <div style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', color: 'white' }}>
        <h2>Chào mừng!</h2>
      </div>
    </div>
  );
}

Giải thích:

  • Prop fill chỉ định rằng ảnh sẽ có kích thước width: 100%; height: 100% so với phần tử cha gần nhất có position khác static.
  • Container (div bọc ngoài) được đặt position: 'relative' và có kích thước cụ thể (width, height) để component Image biết kích thước nó cần lấp đầy.
  • style={{ objectFit: 'cover' }} được sử dụng để đảm bảo ảnh không bị méo và bao phủ toàn bộ container, cắt bớt phần thừa nếu cần.
Prop priority

Theo mặc định, next/image sử dụng lazy loading. Điều này tốt cho hầu hết các ảnh, nhưng với những ảnh quan trọng nằm ở phần đầu trang (above the fold) và ảnh hưởng lớn đến điểm LCP (Largest Contentful Paint), bạn muốn chúng được tải ngay lập tức.

Prop priority làm chính xác điều đó. Khi được thêm vào, ảnh sẽ được tải ưu tiên, bỏ qua lazy loading. Chỉ nên sử dụng prop này cho một hoặc hai ảnh quan trọng nhất trên mỗi trang.

import Image from 'next/image';

function HeroImageWithPriority() {
  return (
    <Image
      src="/images/main-banner.jpg"
      alt="Banner chính của trang"
      width={1600}
      height={800}
      priority // Đánh dấu ảnh này cần được tải ngay lập tức
    />
  );
}

Giải thích:

  • Việc thêm prop priority (không cần gán giá trị) báo cho Next.js biết đây là ảnh quan trọng. Next.js sẽ tối ưu hóa để tải ảnh này nhanh nhất có thể, thường bằng cách preloading.
  • Chỉ dùng cho ảnh hiển thị ban đầu khi người dùng truy cập trang.
Prop placeholder

Prop placeholder cho phép bạn hiển thị một cái gì đó trong khi ảnh chính đang được tải, giúp cải thiện trải nghiệm người dùng (perceived performance). Có hai giá trị phổ biến:

  • "empty": Dành sẵn một khoảng trống có kích thước của ảnh.
  • "blur": Hiển thị một phiên bản ảnh mờ nhỏ (được tạo tự động từ src nếu là ảnh tĩnh, hoặc bạn có thể cung cấp blurDataURL riêng cho ảnh động/remote) trong khi ảnh chính đang tải. Điều này mang lại hiệu ứng chuyển tiếp mượt mà hơn.
import Image from 'next/image';

function ProductCardImage() {
  return (
    <Image
      src="/images/product-thumbnail.jpg"
      alt="Ảnh sản phẩm"
      width={250}
      height={180}
      placeholder="blur" // Hiển thị ảnh mờ trong khi tải
      // Đối với ảnh remote, bạn cần cung cấp blurDataURL:
      // blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0...=="
    />
  );
}

Giải thích:

  • placeholder="blur" là cách phổ biến nhất để tạo hiệu ứng tải mượt mà.
  • Nếu ảnh là file tĩnh trong public, Next.js có thể tự động tạo blurDataURL.
  • Đối với ảnh remote hoặc ảnh động (.gif), bạn cần tạo và cung cấp blurDataURL thủ công (thường là một base64 string của một ảnh placeholder rất nhỏ).

Tổng kết sức mạnh của next/image

Sử dụng next/image thay vì thẻ <img> truyền thống mang lại những lợi ích to lớn:

  • Tự động tối ưu hóa hiệu suất: Giảm kích thước file, sử dụng định dạng hiện đại, lazy loading mặc định.
  • Cải thiện trải nghiệm người dùng: Ngăn chặn layout shift, tải nhanh hơn, hiệu ứng placeholder mượt mà.
  • Nâng cao điểm SEO: Tốc độ tải trang và các chỉ số Core Web Vitals (CLS, LCP) được cải thiện đáng kể, ảnh hưởng tích cực đến thứ hạng tìm kiếm.
  • Tiết kiệm công sức: Hầu hết quá trình tối ưu hóa diễn ra tự động, giúp bạn tập trung vào việc phát triển tính năng.

Việc chuyển đổi từ <img> sang next/image có thể tốn chút thời gian ban đầu để làm quen với các props như width, height, fill, nhưng những lợi ích mà nó mang lại cho hiệu suất và trải nghiệm người dùng là vô giá.

Hãy luôn nhớ sử dụng next/image cho tất cả hình ảnh trong các dự án Next.js của bạn! Nó là một công cụ mạnh mẽ giúp website của bạn trở nên nhanh hơn, mượt mà hơn và thân thiện hơn với người dùng và công cụ tìm kiếm.

Chúc bạn thành công trong việc tối ưu hóa hình ảnh! Hẹn gặp lại ở bài tiếp theo!

Comments

There are no comments at the moment.