Cải thiện hiệu suất tải trang Next.js và Gatsby với tính năng phân đoạn chi tiết

Chiến lược phân đoạn gói web mới hơn trong Next.js và Gatsby giúp giảm thiểu mã trùng lặp để cải thiện hiệu suất tải trang.

Chrome đang cộng tác với các công cụ và khung trong hệ sinh thái nguồn mở JavaScript. Chúng tôi đã thêm một số tính năng tối ưu hoá mới hơn gần đây để cải thiện hiệu suất tải của Next.jsGatsby. Bài viết này đề cập đến chiến lược phân đoạn chi tiết đã cải tiến hiện được vận chuyển theo mặc định trong cả hai khung.

Giới thiệu

Giống như nhiều khung web, Next.js và Gatsby sử dụng webpack làm trình đóng gói chính. webpack v3 đã ra mắt CommonsChunkPlugin để có thể xuất các mô-đun được chia sẻ giữa các điểm truy cập khác nhau trong một (hoặc một vài) đoạn "commons" (hoặc nhiều đoạn). Mã dùng chung có thể được tải xuống riêng và sớm được lưu trữ trong bộ nhớ đệm của trình duyệt, từ đó có thể cải thiện hiệu suất tải.

Mẫu này trở nên phổ biến với nhiều khung ứng dụng trang đơn sử dụng điểm nhập và cấu hình gói có dạng như sau:

Cấu hình gói và điểm truy cập phổ biến

Mặc dù thiết thực, nhưng khái niệm nhóm tất cả mã mô-đun dùng chung thành một phần duy nhất vẫn có một số hạn chế riêng. Bạn có thể tải các mô-đun không được chia sẻ ở mọi điểm truy cập xuống cho các tuyến không sử dụng điểm đến đó, dẫn đến việc mã được tải xuống nhiều hơn mức cần thiết. Ví dụ: khi tải phân đoạn common, page1 sẽ tải mã cho moduleC mặc dù page1 không sử dụng moduleC. Vì lý do này, cùng một số lý do khác, webpack v4 đã xoá trình bổ trợ và thay bằng một trình bổ trợ mới: SplitChunksPlugin.

Cải thiện việc phân đoạn

Chế độ cài đặt mặc định của SplitChunksPlugin phù hợp với hầu hết người dùng. Nhiều phần phân tách được tạo tuỳ thuộc vào một số conditions để ngăn việc tìm nạp mã trùng lặp trên nhiều tuyến.

Tuy nhiên, nhiều khung web sử dụng trình bổ trợ này vẫn tuân theo phương pháp "single-commons" để phân tách. Ví dụ: Next.js sẽ tạo gói commons chứa bất kỳ mô-đun nào được sử dụng trong hơn 50% số trang và tất cả các phần phụ thuộc khung (react, react-dom, v.v.).

const splitChunksConfigs = {
  …
  prod: {
    chunks: 'all',
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: 'commons',
        chunks: 'all',
        minChunks: totalPages > 2 ? totalPages * 0.5 : 2,
      },
      react: {
        name: 'commons',
        chunks: 'all',
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler|use-subscription)[\\/]/,
      },
    },
  },

Mặc dù việc đưa mã phụ thuộc vào khung vào một phân đoạn dùng chung đồng nghĩa với việc mã này có thể được tải xuống và lưu vào bộ nhớ đệm cho bất kỳ điểm truy cập nào, nhưng việc phỏng đoán dựa trên mức sử dụng bao gồm các mô-đun phổ biến được sử dụng trong hơn một nửa số trang là không hiệu quả. Việc sửa đổi tỷ lệ này sẽ chỉ dẫn đến một trong hai kết quả:

  • Nếu bạn giảm tỷ lệ, nhiều mã không cần thiết sẽ được tải xuống.
  • Nếu bạn tăng tỷ lệ này, thì sẽ có nhiều mã trùng lặp trên nhiều tuyến.

Để giải quyết vấn đề này, Next.js đã sử dụng một cấu hình khác cho SplitChunksPlugin để giảm mã không cần thiết cho bất kỳ tuyến nào.

  • Mọi mô-đun bên thứ ba đủ lớn (lớn hơn 160 KB) sẽ được chia thành từng phân đoạn riêng
  • Một phân đoạn frameworks riêng được tạo cho các phần phụ thuộc khung (react, react-dom, v.v.)
  • Tạo nhiều đoạn được chia sẻ tuỳ theo nhu cầu (tối đa 25)
  • Đã thay đổi kích thước tối thiểu để tạo một phân đoạn thành 20 KB

Chiến lược chia nhỏ dữ liệu chi tiết này mang lại các lợi ích sau:

  • Thời gian tải trang được cải thiện. Việc tạo nhiều đoạn dùng chung (thay vì một đoạn) sẽ giúp giảm thiểu số lượng mã không cần thiết (hoặc trùng lặp) cho mọi điểm nhập.
  • Cải thiện chức năng lưu vào bộ nhớ đệm trong khi di chuyển. Việc tách các thư viện lớn và phần phụ thuộc khung thành các phần riêng biệt giúp giảm khả năng vô hiệu hoá bộ nhớ đệm vì cả hai đều ít có khả năng thay đổi cho đến khi nâng cấp.

Bạn có thể xem toàn bộ cấu hình mà Next.js đã sử dụng trong webpack-config.ts.

Yêu cầu HTTP khác

SplitChunksPlugin đã xác định cơ sở cho việc phân đoạn chi tiết và việc áp dụng phương pháp này vào một khung như Next.js không phải là một khái niệm hoàn toàn mới. Tuy nhiên, nhiều khung vẫn tiếp tục sử dụng một chiến lược gói "chung" duy nhất và phỏng đoán vì một vài lý do. Điều này bao gồm mối lo ngại về việc có thêm nhiều yêu cầu HTTP có thể ảnh hưởng tiêu cực đến hiệu suất trang web.

Các trình duyệt chỉ có thể mở một số lượng giới hạn kết nối TCP tới một nguồn gốc duy nhất (6 đối với Chrome). Vì vậy, việc giảm thiểu số lượng phân đoạn do trình đóng gói đưa ra có thể đảm bảo tổng số yêu cầu vẫn nằm dưới ngưỡng này. Tuy nhiên, điều này chỉ đúng với HTTP/1.1. Phương pháp ghép kênh trong HTTP/2 cho phép truyền song song nhiều yêu cầu thông qua một kết nối duy nhất qua một nguồn gốc. Nói cách khác, chúng ta thường không cần lo lắng về việc giới hạn số lượng đoạn do trình đóng gói phát ra.

Tất cả các trình duyệt chính đều hỗ trợ HTTP/2. Nhóm Chrome và Next.js muốn xem liệu việc tăng số lượng yêu cầu bằng cách chia gói "commons" duy nhất của Next.js thành nhiều phần dùng chung có ảnh hưởng đến hiệu suất tải theo bất kỳ cách nào hay không. Họ bắt đầu bằng cách đo lường hiệu suất của một trang web, đồng thời sửa đổi số lượng tối đa các yêu cầu song song bằng cách sử dụng thuộc tính maxInitialRequests.

Hiệu suất tải trang với số lượng yêu cầu tăng

Trung bình ba lần chạy nhiều lần thử trên một trang web, thời gian load, bắt đầu kết xuấtHiển thị nội dung đầu tiên đều giữ nguyên khi thay đổi số lượng yêu cầu ban đầu tối đa (từ 5 đến 15). Điều thú vị là chúng tôi nhận thấy sự hao tổn nhỏ về hiệu suất chỉ sau khi phân chia linh hoạt thành hàng trăm yêu cầu.

Hiệu suất tải trang với hàng trăm yêu cầu

Điều này cho thấy việc duy trì dưới ngưỡng đáng tin cậy (20~25 yêu cầu) đã tạo ra sự cân bằng hợp lý giữa hiệu suất tải và hiệu quả lưu vào bộ nhớ đệm. Sau một số hoạt động kiểm thử cơ sở, 25 đã được chọn làm số lượng maxInitialRequest.

Việc sửa đổi số lượng yêu cầu tối đa diễn ra song song sẽ dẫn đến nhiều hơn một gói dùng chung và việc phân tách các yêu cầu đó một cách phù hợp cho từng điểm truy cập đã làm giảm đáng kể số lượng mã không cần thiết cho cùng một trang.

Giảm tải trọng JavaScript bằng cách tăng phân đoạn

Thử nghiệm này chỉ xoay quanh việc sửa đổi số lượng yêu cầu để xem liệu có tác động tiêu cực nào đến hiệu suất tải trang hay không. Kết quả cho thấy việc đặt maxInitialRequests thành 25 trên trang thử nghiệm là tối ưu vì việc này đã giảm kích thước tải trọng JavaScript mà không làm chậm trang. Tổng lượng JavaScript cần thiết để cấp nước cho trang vẫn không thay đổi. Điều này giải thích tại sao hiệu suất tải trang chưa hẳn cải thiện khi số lượng mã giảm.

webpack sử dụng 30 KB làm kích thước tối thiểu mặc định để tạo phân đoạn. Tuy nhiên, việc kết hợp giá trị maxInitialRequests là 25 với kích thước tối thiểu 20 KB giúp cải thiện khả năng lưu vào bộ nhớ đệm.

Giảm kích thước bằng các phân đoạn chi tiết

Nhiều khung, bao gồm cả Next.js, dựa vào việc định tuyến phía máy khách (do JavaScript xử lý) để chèn các thẻ tập lệnh mới hơn cho mọi quá trình chuyển đổi tuyến đường. Nhưng làm cách nào để chúng xác định trước các phân đoạn động này trong thời gian xây dựng?

Next.js sử dụng tệp kê khai bản dựng phía máy chủ để xác định xem đoạn đầu ra nào được các điểm truy cập khác nhau sử dụng. Để cung cấp thông tin này cho ứng dụng khách, một tệp kê khai bản dựng phía máy khách rút gọn đã được tạo để ánh xạ tất cả các phần phụ thuộc cho mọi điểm truy cập.

// Returns a promise for the dependencies for a particular route
getDependencies (route) {
  return this.promisedBuildManifest.then(
    man => (man[route] && man[route].map(url => `/_next/${url}`)) || []
  )
}
Xuất ra nhiều đoạn được chia sẻ trong ứng dụng Next.js.

Chiến lược phân đoạn chi tiết mới hơn này lần đầu được triển khai trong Next.js sau một cờ và được thử nghiệm trên một số người sử dụng sớm. Nhiều người nhận thấy tổng số JavaScript được dùng cho toàn bộ trang web của họ đã giảm đáng kể:

Trang web Tổng thay đổi về JS Mức chênh lệch (%)
https://www.barnebys.com/ -238 KB Giảm 23%
https://sumup.com/ -220 KB Giảm 30%
https://www.hashicorp.com/ Dưới 11 MB Giảm 71%
Giảm kích thước JavaScript – trên mọi tuyến (nén)

Phiên bản cuối cùng được vận chuyển trong phiên bản 9.2 theo mặc định.

Gatsby

Gatsby dùng để tuân theo cùng một phương pháp dùng phương pháp phỏng đoán dựa trên mức sử dụng để xác định các mô-đun phổ biến:

config.optimization = {
  …
  splitChunks: {
    name: false,
    chunks: `all`,
    cacheGroups: {
      default: false,
      vendors: false,
      commons: {
        name: `commons`,
        chunks: `all`,
        // if a chunk is used more than half the components count,
        // we can assume it's pretty global
        minChunks: componentsCount > 2 ? componentsCount * 0.5 : 2,
      },
      react: {
        name: `commons`,
        chunks: `all`,
        test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
      },

Bằng cách tối ưu hoá cấu hình gói web để áp dụng chiến lược phân đoạn chi tiết tương tự, họ cũng nhận thấy mức giảm đáng kể về JavaScript trong nhiều trang web lớn:

Trang web Tổng thay đổi về JS Mức chênh lệch (%)
https://www.gatsbyjs.org/ -680 KB Giảm 22%
https://www.thirdandgrove.com/ -390 KB Dưới 25%
https://ghost.org/ -1,1 MB Giảm 35%
https://reactjs.org/ -80 Kb -8%
Giảm kích thước JavaScript – trên mọi tuyến (nén)

Hãy xem phần PR để hiểu cách họ triển khai logic này vào cấu hình gói web, được vận chuyển theo mặc định trong phiên bản 2.20.7.

Kết luận

Khái niệm về việc vận chuyển các đoạn chi tiết không dành riêng cho Next.js, Gatsby hay thậm chí là webpack. Mọi người nên cân nhắc việc cải thiện chiến lược phân đoạn của ứng dụng nếu chiến lược này tuân theo phương pháp gói "chung" lớn, bất kể trình đóng gói khung hoặc mô-đun được sử dụng là gì.

  • Nếu bạn muốn xem các tính năng tối ưu hoá phân đoạn tương tự được áp dụng cho ứng dụng vanilla React, hãy xem ứng dụng React mẫu này. Ứng dụng sử dụng phiên bản đơn giản hoá của chiến lược phân đoạn chi tiết và có thể giúp bạn bắt đầu áp dụng cùng một loại logic cho trang web của mình.
  • Đối với Rollup, các phần được tạo chi tiết theo mặc định. Hãy xem manualChunks nếu bạn muốn định cấu hình hành vi theo cách thủ công.