[Next.js Tutorial] Tối ưu Image, Font, và Script: Tăng tốc độ tải trang

Trong thế giới phát triển web hiện đại, tốc độ không chỉ là một lợi thế, mà là yếu tố sống còn. Một trang web tải chậm không chỉ khiến người dùng thất vọng bỏ đi mà còn bị các công cụ tìm kiếm như Google "ghẻ lạnh", ảnh hưởng trực tiếp đến thứ hạng SEO và doanh thu. May mắn thay, với Next.js, chúng ta được trang bị những công cụ cực kỳ mạnh mẽ để giải quyết triệt để các vấn đề hiệu năng cốt lõi.

Tối ưu Image, Font, và Script trong Nextjs

Bài viết này sẽ đi sâu vào ba "hung thần" gây chậm trang web: Hình ảnh, Font chữ, và các đoạn mã Script từ bên thứ ba. Chúng ta sẽ khám phá cách "thuần hóa" chúng bằng các component chuyên dụng của Next.js: next/image, next/font, và next/script. Hãy sẵn sàng để đưa ứng dụng Next.js của bạn lên một tầm cao mới về tốc độ và trải nghiệm người dùng! ⚡️

1. next/image: "Phù thủy" tối ưu hóa hình ảnh 🖼️

Hình ảnh thường là tài sản nặng nhất trên một trang web. Tải một bức ảnh 5MB cho màn hình điện thoại là một sự lãng phí tài nguyên khủng khiếp. Component next/image ra đời để giải quyết vấn đề này một cách thông minh và tự động. Nó không chỉ là một thẻ <img> thông thường, mà là một cỗ máy tối ưu hóa hình ảnh toàn diện.

Vấn đề cố hữu của thẻ <img> truyền thống

Thẻ <img> có khá nhiều điểm yếu nếu không được tối ưu lại:

  • Tải kích thước gốc: Tải một ảnh 4K cho một div rộng 300px, gây lãng phí băng thông.
  • Không có lazy loading mặc định: Tất cả hình ảnh đều được tải ngay lập tức, kể cả những ảnh cuối trang, làm chậm quá trình tải ban đầu.
  • Layout Shift (CLS): Trình duyệt không biết kích thước ảnh cho đến khi nó được tải xong, gây ra tình trạng "nhảy trang" khó chịu khi nội dung bị đẩy xuống.
  • Không tự động chuyển đổi định dạng: Không tận dụng được các định dạng ảnh hiện đại như WebP hay AVIF có khả năng nén tốt hơn.

Sức mạnh của next/image

next/image giải quyết tất cả các vấn đề trên một cách "thần kỳ":

  • Tự động Resizing: Next.js tự động tạo ra nhiều phiên bản của một ảnh với các kích thước khác nhau và chỉ phục vụ phiên bản phù hợp nhất cho thiết bị của người dùng.
  • Lazy Loading mặc định: Hình ảnh sẽ chỉ được tải khi người dùng cuộn đến gần chúng, giúp tăng tốc độ tải trang ban đầu một cách đáng kể.
  • Chống Layout Shift: Bằng cách yêu cầu widthheight, next/image sẽ giữ chỗ cho hình ảnh trước khi nó được tải, loại bỏ hoàn toàn chỉ số Cumulative Layout Shift (CLS).
  • Định dạng ảnh thế hệ mới: Tự động phục vụ ảnh dưới định dạng WebP hoặc AVIF nếu trình duyệt hỗ trợ, giúp giảm kích thước file mà vẫn giữ nguyên chất lượng.
  • Ưu tiên tải (Priority Loading): Đối với những hình ảnh quan trọng ở màn hình đầu tiên (above-the-fold) như hero banner, bạn có thể thêm prop priority để báo cho Next.js tải nó trước, cải thiện mạnh mẽ chỉ số Largest Contentful Paint (LCP).

Cách sử dụng hiệu quả next/image

Với ảnh local (trong thư mục public hoặc src):

import Image from 'next/image'
import heroBanner from '../public/images/hero-banner.jpg'

function HomePage() {
  return (
    <Image
      src={heroBanner}
      alt="Mô tả hình ảnh"
      width={1200}
      height={600}
      priority // Ưu tiên tải cho ảnh LCP
      placeholder="blur" // Hiệu ứng mờ khi ảnh đang tải
      // sizes giúp trình duyệt chọn ảnh phù hợp với các kích cỡ màn hình khác nhau
      sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
    />
  )
}

Với ảnh từ nguồn bên ngoài (CDN):

Trước tiên, bạn cần cấu hình domain của ảnh trong next.config.js:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
        port: '',
        pathname: '/**',
      },
    ],
  },
}

Sau đó sử dụng trong component:

import Image from 'next/image'

function PostCard({ post }) {
  return (
    <Image
      src={post.imageUrl} // "https://images.unsplash.com/photo-..."
      alt={post.title}
      width={500}
      height={300}
      loading="lazy" // Mặc định, nhưng  thể ghi 
    />
  )
}

💡 Mẹo hay: Luôn cung cấp widthheight chính xác. Sử dụng prop sizes để tối ưu hóa việc chọn ảnh trên các thiết bị khác nhau và dùng priority cho hình ảnh quan trọng nhất trên trang.

2. next/font: Giải pháp Font chữ không còn Layout Shift ✍️

Web font mang lại tính thẩm mỹ cao, nhưng chúng cũng là một nguồn gây ra các vấn đề về hiệu năng như Flash of Invisible Text (FOIT) hoặc Flash of Unstyled Text (FOUT), và tệ hơn là Layout Shift khi font chữ được tải xong và thay thế font hệ thống.

next/font là một cuộc cách mạng trong việc quản lý font. Nó tự động tối ưu hóa và tự host (self-host) các font chữ cho bạn, loại bỏ hoàn toàn các vấn đề trên.

Lợi ích vượt trội của next/font

Những lý do mà bạn nên sử dụng next/font ngay cho dự án của mình:

  • Tự động Self-hosting: Thay vì phải gửi yêu cầu đến Google Fonts mỗi lần tải trang, next/font sẽ tải font về và phục vụ nó cùng với các tài sản tĩnh khác của bạn. Điều này loại bỏ một yêu cầu mạng khứ hồi (round-trip), giúp giảm thời gian chặn hiển thị (render-blocking).
  • Zero Layout Shift: Đây là tính năng "ăn tiền" nhất. next/font sử dụng thuộc tính CSS size-adjust để điều chỉnh font hệ thống dự phòng sao cho nó chiếm không gian y hệt như web font sẽ tải. Kết quả? Khi web font được tải xong, không có bất kỳ sự xê dịch nào xảy ra.
  • Hiệu năng cao: next/font tự động thêm font-display: optional; (hoặc swap) vào CSS, giúp trình duyệt hiển thị văn bản ngay lập tức bằng font dự phòng.

Cách sử dụng next/font

Với Google Fonts:

// In your layout.js or _app.js
import { Inter, Roboto_Mono } from 'next/font/google'

// Khởi tạo font với các tùy chọn
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter', // Sử dụng với CSS Variables
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-roboto-mono',
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}

Sau đó trong file globals.css của bạn:

body {
  font-family: var(--font-inter), sans-serif;
}

h1,
h2,
code {
  font-family: var(--font-roboto-mono), monospace;
}

Với Font Local:

// In your layout.js or _app.js
import localFont from 'next/font/local'

const myFont = localFont({
  src: './MyCustomFont.woff2',
  display: 'swap',
  variable: '--font-my-custom',
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={myFont.variable}>
      <body>{children}</body>
    </html>
  )
}

💡 Mẹo hay: Luôn sử dụng next/font thay vì thẻ <link> truyền thống trong _document.js để nhúng font. Nó mang lại lợi ích về hiệu năng và trải nghiệm người dùng mà không cần nỗ lực cấu hình phức tạp.

3. next/script: "Nhạc trưởng" quản lý Script bên thứ ba 📜

Các đoạn mã script từ bên thứ ba (analytics, chatbot, advertisement, Google Tag Manager) là một trong những thủ phạm hàng đầu làm chậm website và ảnh hưởng đến chỉ số Total Blocking Time (TBT). Chúng thường chặn luồng chính (main thread), làm trì hoãn khả năng tương tác của người dùng.

Component next/script cho phép bạn kiểm soát chính xác thời điểm và cách thức các script này được tải và thực thi.

Các chiến lược tải (Loading Strategies)

next/script cung cấp một prop strategy cực kỳ mạnh mẽ:

  • strategy="beforeInteractive":

    • Khi nào dùng: Chỉ dành cho các script cực kỳ quan trọng phải được thực thi trước khi trang có thể tương tác, ví dụ như script quản lý cookie consent.
    • Lưu ý: Sử dụng rất cẩn thận vì nó có thể chặn hiển thị trang.
  • strategy="afterInteractive" (Mặc định):

    • Khi nào dùng: Lựa chọn hoàn hảo cho hầu hết các script. Script sẽ được tải và thực thi ngay sau khi trang đã có thể tương tác (hydrated).
    • Ví dụ: Google Analytics, Google Tag Manager.
  • strategy="lazyOnload":

    • Khi nào dùng: Dành cho các script có độ ưu tiên thấp, có thể chờ để tải sau khi tất cả các tài nguyên khác đã tải xong và trình duyệt đang "rảnh rỗi".
    • Ví dụ: Chatbot (Intercom, Crisp), các widget mạng xã hội.

Cách sử dụng next/script

import Script from 'next/script'

function MyPage() {
  return (
    <div>
      <h1>My Page</h1>
      {/* Google Tag Manager - Tải sau khi trang tương tác */}
      <Script
        src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
        strategy="afterInteractive"
      />
      <Script id="google-analytics" strategy="afterInteractive">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', 'GA_MEASUREMENT_ID');
        `}
      </Script>

      {/* Intercom Chat Widget - Tải khi trình duyệt rảnh */}
      <Script
        src="https://widget.intercom.io/widget/APP_ID"
        strategy="lazyOnload"
      />
    </div>
  )
}

Tối ưu hóa nâng cao với Web Workers (Partytown)

Đối với các script nặng, bạn có thể đẩy chúng ra khỏi luồng chính hoàn toàn bằng cách chạy chúng trong một Web Worker. Next.js tích hợp với Partytown để làm điều này một cách dễ dàng.

Để kích hoạt, trong next.config.js:

// next.config.js
module.exports = {
  experimental: {
    workerThreads: true,
    cpus: 1, // Giới hạn số lượng worker để tránh tiêu tốn tài nguyên
  },
}

Sau đó sử dụng strategy="worker":

<Script src="https://example.com/heavy-script.js" strategy="worker" />

Hành động này sẽ chuyển gánh nặng xử lý của script đó sang một luồng nền, giữ cho giao diện người dùng của bạn luôn mượt mà và phản hồi nhanh chóng.

Kết luận: Tối ưu hóa không phải việc làm một lần

Việc tối ưu hóa không phải là một công việc làm một lần rồi thôi, mà là một quá trình liên tục. Tuy nhiên, bằng cách nắm vững và áp dụng ba "vũ khí" mạnh mẽ mà Next.js cung cấp:

  • next/image: Để phục vụ hình ảnh nhanh, nhẹ và không gây layout shift.
  • next/font: Để có font chữ đẹp mà không ảnh hưởng đến hiệu năng và trải nghiệm người dùng.
  • next/script: Để kiểm soát các script của bên thứ ba và giữ cho luồng chính luôn thông thoáng.

Bạn đã có trong tay bộ công cụ cần thiết để xây dựng những ứng dụng web không chỉ đẹp về giao diện, mạnh về tính năng mà còn siêu nhanh về tốc độ. Hãy bắt đầu kiểm tra lại dự án của mình ngay hôm nay và áp dụng những kỹ thuật này, bạn sẽ thấy sự khác biệt rõ rệt trên các công cụ đo lường như PageSpeed Insights và quan trọng hơn cả là trong sự hài lòng của người dùng cuối.

Bài viết liên quan

[Next.js Tutorial] Cấu trúc thư mục và Routing cơ bản trong dự án

Làm thế nào để tổ chức dự án Next.js hiệu quả? Hướng dẫn chi tiết về cấu trúc thư mục và hệ thống routing cơ bản, giúp bạn xây dựng website Next.js nhanh chóng và chuẩn mực.

[Next.js Tutorial] Data Fetching trong Server Components: Cách tối ưu hiệu quả

Tìm hiểu sâu về cách Data Fetching hoạt động với Server Components trong Next.js. Khám phá các phương pháp tốt nhất để truy vấn dữ liệu, tối ưu hiệu suất và xây dựng ứng dụng nhanh chóng, mượt mà hơn.

[Next.js Tutorial] Dynamic Routes: Hướng dẫn cách dùng và cách tối ưu

Tìm hiểu cách tạo và quản lý Dynamic Routes trong Next.js (App Router). Bài viết hướng dẫn chi tiết từng bước, giúp bạn xây dựng các trang web động hiệu quả và chuẩn SEO.

[Next.js Tutorial] Navigation và Linking: Cách sử dụng cho hiệu suất tối ưu

Bài viết này sẽ giúp bạn làm chủ Navigation và Linking trong Next.js một cách dễ dàng. Tìm hiểu chi tiết cách sử dụng Next/link và useRouter để điều hướng trang, tối ưu hiệu suất ứng dụng của bạn.