Giảm dung lượng Bundle Next.js với React Server Components

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Xu hướng tiến tới Kiến trúc Server-First

Trong hơn một thập kỷ, việc mở rộng ứng dụng Frontend quy mô lớn với React hầu như chỉ tồn tại trên trình duyệt. Chúng ta đã quen với việc gửi những gói JavaScript (bundle) khổng lồ, buộc thiết bị của người dùng phải phân tích, biên dịch và thực thi mã trước khi hiển thị bất kỳ điểm ảnh nào. Sau khi triển khai ba dự án thực tế trên Next.js App Router trong sáu tháng qua, tôi đã thấy điểm hiệu năng Lighthouse nhảy vọt từ mức 60 lên mức ổn định trên 98 nhờ React Server Components (RSC).

Tại sao các SPA truyền thống lại gặp rào cản

Trong một ứng dụng Single Page Application (SPA) tiêu chuẩn, mỗi tính năng đều làm tăng thêm dung lượng. Nếu bạn import một thư viện tiện ích nặng như date-fns chỉ cho một component nhỏ, trình duyệt vẫn phải tải xuống toàn bộ thư viện đó. Sự phình to này dẫn đến chỉ số ‘Time to Interactive’ (TTI) chậm chạp và ảnh hưởng xấu đến SEO. Theo kinh nghiệm của tôi, một trang dashboard điển hình có thể dễ dàng tăng vọt lên 500KB JavaScript ngay cả trước khi bạn thêm logic nghiệp vụ.

React Server Components đảo ngược tình thế này. Chúng thực thi nghiêm ngặt trên máy chủ và gửi một mô tả giao diện giống như JSON đến client thay vì mã nguồn thô. Kết quả là gì? Dung lượng JavaScript cho component đó trên thiết bị người dùng chính xác là 0 byte.

Đây không chỉ là một tính năng mới; đó là một sự thay đổi về tư duy cần thiết để xây dựng các ứng dụng web mang lại cảm giác tức thì ngay cả trên các điện thoại Android giá rẻ hoặc kết nối 4G chập chờn.

Thiết lập Stack Next.js Hiện đại

App Router là cách tiêu chuẩn để tận dụng RSC hiện nay. Nó đã ổn định kể từ phiên bản 13.4 và coi server là “công dân hạng nhất”. Trong môi trường này, mọi tệp bạn tạo bên trong thư mục app/ đều mặc định là một Server Component.

Khởi đầu nhanh

Khởi tạo một dự án mới mất chưa đầy một phút. Hãy chạy lệnh này để có ngay cấu trúc chuẩn:

npx create-next-app@latest my-rsc-app --typescript --tailwind --eslint

Chọn “Yes” cho App Router trong quá trình cài đặt. Điều này sẽ thiết lập cấu trúc thư mục giúp xử lý việc tách biệt logic giữa server và client, đồng thời giúp bạn ngừng dự đoán và bắt đầu viết code an toàn hơn.

Kiến trúc cho các Bundle 0 KB

Thành công phụ thuộc vào việc làm chủ Design Pattern trong TypeScript để chia giao diện người dùng thành các phần tương tác và tĩnh. Trong ứng dụng thực tế gần đây nhất, chúng tôi đã chuyển thành công 75% logic component lên server, chỉ để lại các component “lá” (leaf components) xử lý đầu vào của người dùng.

Tận dụng mặc định từ Server

Một tệp như app/inventory/[id]/page.tsx mặc định là một Server Component. Bạn có thể fetch dữ liệu trực tiếp bằng async/await, hoàn toàn loại bỏ nhu cầu sử dụng useEffect hoặc quản lý state phức tạp cho lần tải đầu tiên.

// app/products/page.tsx
import { db } from '@/lib/db';

export default async function ProductsPage() {
  // Lệnh gọi cơ sở dữ liệu này nằm tại server
  const products = await db.product.findMany(); 

  return (
    <div>
      <h1>Danh mục sản phẩm</h1>
      <ul>
        {products.map((p) => (
          <li key={p.id}>{p.name} - ${p.price}</li>
        ))}
      </ul>
    </div>
  );
}

Client kết nối cơ sở dữ liệu nhạy cảm sẽ không bao giờ xuất hiện ở trình duyệt. Người dùng của bạn nhận được HTML sạch và một chút metadata, giúp bảo mật thông tin và giữ cho bundle luôn gọn nhẹ.

Xử lý tương tác

Server Components không thể lắng nghe các sự kiện click hoặc quản lý State trong React cục bộ. Khi bạn cần một nút gạt (toggle) hoặc một biểu mẫu (form), hãy tạo một Client Component bằng cách đặt dòng "use client" ở đầu tệp.

"use client";

import { useState } from 'react';

export default function Counter({ initialCount }) {
  const [count, setCount] = useState(initialCount);

  return (
    <button onClick={() => setCount(count + 1)}>
      Đã chọn {count} mục
    </button>
  );
}

Hãy giữ các client component này càng nhỏ càng tốt. Hãy fetch phần lớn dữ liệu trong Server Component cha, sau đó truyền các props đơn giản xuống các “lá” tương tác này.

Truy xuất dữ liệu thông minh

Chúng ta từng gặp khó khăn với hiện tượng “thác nước” (waterfalls), nơi các component lồng nhau kích hoạt các lệnh gọi API tuần tự và chậm chạp. RSC cho phép bạn fetch dữ liệu chính xác tại nơi nó được sử dụng. Next.js tự động loại bỏ các yêu cầu trùng lặp (deduplicates), vì vậy bạn không cần lo lắng về việc gọi API hai lần cho cùng một dữ liệu người dùng trên một trang duy nhất.

Cách tiếp cận này loại bỏ tình trạng “prop drilling”. Bạn không còn cần phải truyền một đối tượng người dùng qua năm tầng component chỉ vì phần footer cần một ảnh đại diện.

Đo lường tác động

Bạn không thể quản lý những gì bạn không đo lường. Bạn cần xác nhận rằng kiến trúc của mình thực sự tiết kiệm dung lượng byte chứ không phải vô tình làm rò rỉ mã phía server vào client.

Sử dụng Bundle Analyzer

Tôi thường dựa vào @next/bundle-analyzer để trực quan hóa kết quả đầu ra. Nó làm nổi bật chính xác thư viện nào đang tiêu tốn “ngân sách” dung lượng của bạn, một bước quan trọng để tối ưu hóa Build Pipeline và đảm bảo hiệu suất. Cài đặt nó bằng lệnh:

npm install @next/bundle-analyzer

Cập nhật next.config.js để kích hoạt:

const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
})

module.exports = withBundleAnalyzer({})

Chạy ANALYZE=true npm run build để xem kết quả. Trong một trường hợp, việc chuyển một bảng dữ liệu phức tạp sang Server Component đã loại bỏ 52KB JavaScript nén (gzipped) – chủ yếu là các công cụ định dạng nặng – khỏi bundle chính của chúng tôi.

Kiểm tra RSC Payload

Hãy nhìn vào tab Network trong DevTools của trình duyệt khi điều hướng. Bạn sẽ thấy các yêu cầu có kiểu application/octet-stream. Đây chính là RSC payload. Nó thường nhỏ hơn 80-90% so với lượng JavaScript cần thiết để hiển thị cùng một nội dung thông qua các phương pháp phía client truyền thống.

Thêm “Lưới an toàn”

Vô tình import một bí mật cơ sở dữ liệu vào Client Component là một rủi ro thường gặp. Hãy sử dụng gói server-only để phát hiện những sai lầm này trong quá trình phát triển:

npm install server-only

Thêm import 'server-only'; vào các tệp tiện ích API hoặc DB của bạn. Nếu một Client Component cố gắng import chúng, quá trình build sẽ thất bại ngay lập tức, giúp ngăn chặn sự rò rỉ bảo mật tiềm ẩn.

Lời kết

React Server Components thay đổi mối quan hệ của chúng ta với trình duyệt. Chúng ta không còn xây dựng các ứng dụng nặng nề chạy trên client; chúng ta đang xây dựng các giao diện tinh gọn được hỗ trợ bởi server. Hiệu năng đạt được là thật, và trải nghiệm lập trình cũng sạch sẽ hơn nhiều một khi bạn vượt qua được rào cản làm quen ban đầu. Hãy bắt đầu bằng cách di chuyển các trang tĩnh nhất của bạn — như blog hoặc danh sách sản phẩm — và quan sát dung lượng bundle dần biến mất.

Share: