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 trong Next.js và Gatsby sẽ 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à các khung trong hệ sinh thái nguồn mở JavaScript. Gần đây đã có một số tính năng tối ưu hoá mới để 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 cải tiến mà hiện đã được 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 nền tảng cốt lõi gói. ra mắt gói webpack v3 CommonsChunkPlugin để có thể mô-đun đầu ra được chia sẻ giữa các điểm truy cập khác nhau trong một (hoặc một vài) "commons" phân đoạn (hoặc phân đoạn). Bạn có thể tải mã dùng chung xuống một cách riêng biệt rồi lưu trữ trong bộ nhớ đệm của trình duyệt ngay từ đầu, nhờ đó, giúp nâng cao 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 áp 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ù thực tế, khái niệm nhóm tất cả mã mô-đun được chia sẻ vào một đoạn duy nhất có hạn chế. 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 mô-đun đó khiến mã được tải xuống nhiều hơn mức cần thiết. Ví dụ: khi page1 tải phân đoạn common, nó sẽ tải mã cho moduleC mặc dù page1 không sử dụng moduleC. Vì lý do này, cùng với một vài lý do khác, webpack v4 đã xoá trình bổ trợ để thay bằng một một: SplitChunksPlugin.

Cải thiện khả năng phân đoạn

Chế độ cài đặt mặc định cho SplitChunksPlugin phù hợp với hầu hết người dùng. Nhiều phần được tách được tạo dựa trên một số điều kiện. để 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 quy tắc "single-commons" tiếp cận phân đoạn chia tách. Ví dụ: Next.js sẽ tạo một gói commons chứa bất kỳ mô-đun nào được sử dụng trên hơn 50% số trang và tất cả các phần phụ thuộc của 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 có nghĩa là mã đó có thể được tải xuống và được lưu vào bộ nhớ đệm cho bất kỳ điểm truy cập nào, suy đoán dựa trên mức sử dụng trong việc bao gồm các mô-đun phổ biến được sử dụng trong hơn nửa trang không hiệu quả lắm. 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ệ này, thì sẽ có nhiều mã không cần thiết được tải xuống hơn.
  • Nếu bạn tăng tỷ lệ này, sẽ có nhiều mã bị 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 phương thức cấu hình choSplitChunksPlugin giúp giảm mã không cần thiết cho mọi tuyến.

  • Bất kỳ mô-đun bên thứ ba nào đủ lớn (lớn hơn 160 KB) đều được tách thành riêng mô-đun riêng phân đoạn
  • Một phân đoạn frameworks riêng biệt được tạo cho các phần phụ thuộc của khung (react, react-dom và v.v.)
  • Số lượng phân đoạn dùng chung được tạo theo nhu cầu (tối đa 25)
  • Đổi kích thước tối thiểu cho một đoạn được tạo thành 20 KB

Chiến lược phân đoạn chi tiết này mang lại những lợi ích sau:

  • Thời gian tải trang được cải thiện. Phát ra nhiều đoạn dùng chung, thay vì một đoạn duy nhất, giảm thiểu số lượng mã không cần thiết (hoặc trùng lặp) cho bất kỳ điểm nhập nào.
  • Cải thiện việc lưu vào bộ nhớ đệm trong quá trình điều hướng. 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 phần này ít có khả năng cho đến khi thực hiện nâng cấp.

Bạn có thể thấy 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à áp dụng phương pháp này cho một 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 chương trình sử dụng một suy nghiệm duy nhất và "commons" vì một số lý do. Điều này bao gồm mối lo ngại rằng càng nhiều yêu cầu HTTP khác 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 (6 đối với Chrome), vì vậy Việc giảm thiểu số lượng phần do trình gói xuất ra có thể đảm bảo rằng tổng số yêu cầu vẫn dưới ngưỡng này. Tuy nhiên, điều này chỉ đúng với HTTP/1.1. Ghép kênh trong HTTP/2 cho phép nhiều yêu cầu được phát trực tuyến song song bằng cách sử dụng một kết nối duy nhất qua một máy chủ 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 phần do bộ gói của chúng tôi tạo ra.

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

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

Trong trung bình ba lần chạy nhiều thử nghiệm trên một trang web, load! start-render và thời gian Hiển thị nội dung đầu tiên vẫn giữ nguyên khi thay đổi giá trị tối đa ban đầu số lượng yêu cầu (từ 5 đến 15). Điều thú vị là chúng tôi chỉ nhận thấy sự hao tổn hiệu suất không đáng kể sau khi chia tách riêng 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ố thử nghiệm 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 xảy ra song song dẫn đến nhiều hơn một gói dùng chung và việc phân tách chúng phù hợp cho từng điểm truy cập sẽ 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 nhờ tăng phân đoạn

Thử nghiệm này chỉ nhằm sửa đổi số lượng yêu cầu để xem liệu sẽ có bất kỳ ảnh hưởng tiêu cực đến hiệu suất tải trang. Kết quả cho thấy việc đặt maxInitialRequests thành 25 trên trang thử nghiệm là tối ưu vì làm giảm kích thước tải trọng JavaScript mà không làm chậm xuống dưới trang. Tổng lượng JavaScript cần thiết để cung cấp đủ lượng dữ liệu cho trang vẫn giữ nguyên điều này giải thích tại sao hiệu suất tải trang không nhất thiết đã cải thiện khi số lượng mã.

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

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 định tuyến phía máy khách (được xử lý bằng JavaScript) để chèn các thẻ tập lệnh mới hơn cho mỗi chuyển đổi định tuyến. Nhưng làm cách nào để họ xác định trước các phần động này tại thời điểm 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 các đoạn được xuất ra được sử dụng bởi các điểm truy cập khác nhau. Để cũng cung cấp thông tin này cho khách hàng, hãy tóm lược phía máy khách tệp kê khai của bản dựng đượ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}`)) || []
  )
}
Kết quả đầu ra của nhiều phân đoạn dùng chung 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 tiên được triển khai trong Next.js sau một cờ, trong đó chiến lược này được thử nghiệm trên số người sử dụng sớm. Nhiều người nhận thấy tổng số JavaScript được dùng cho ứng dụng của họ đã giảm đáng kể toàn bộ trang web:

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/ 11 MB Giảm 71%
Giảm kích thước JavaScript – trên tất cả các tuyến (nén)

Phiên bản cuối cùng được gửi theo mặc định trong phiên bản 9.2.

Gatsby

Gatsby vẫn áp dụng phương pháp tiếp cận dựa trên mức sử dụng phương pháp phỏng đoán để 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 đã giảm đáng kể JavaScript ở 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 tất cả các tuyến (nén)

Hãy xem phần quan hệ công chúng để hiểu cách họ đã triển khai logic này vào cấu hình gói web của họ, đượ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 phân đ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 xem xét 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 một quy tắc chung theo gói, bất kể khung hoặc trình đóng gói mô-đun được sử dụng là gì.

  • Nếu bạn muốn xem các biện pháp tối ưu hoá việc phân đoạn tương tự được áp dụng cho ứng dụng vanilla React, hãy xem biểu mẫu React này ứng dụng. Chiến dịch này sử dụng một phiên bản đơn giản 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 logic vào trang web của mình.
  • Đối với dữ liệu hợp nhất, theo mặc định, các phần được tạo một cách chi tiết. Cần lưu ý đến manualChunks nếu bạn muốn tự mình định cấu hình hành vi.