JavaScript phân tách mã

Việc tải các tài nguyên JavaScript lớn ảnh hưởng đáng kể đến tốc độ trang. Việc tách JavaScript thành các phần nhỏ và chỉ tải những dữ liệu cần thiết để một trang hoạt động trong quá trình khởi động có thể cải thiện đáng kể khả năng phản hồi tải của trang, nhờ đó có thể cải thiện Hoạt động tương tác với NextPaint (INP) của trang.

Khi tải xuống, phân tích cú pháp và biên dịch các tệp JavaScript lớn, trang đó có thể không phản hồi trong một khoảng thời gian. Các phần tử trang sẽ hiển thị vì chúng là một phần của HTML ban đầu của trang và do CSS tạo kiểu. Tuy nhiên, vì JavaScript cần để hỗ trợ các phần tử tương tác đó (cũng như các tập lệnh khác do trang tải) có thể phân tích cú pháp và thực thi JavaScript để các phần tử đó hoạt động. Kết quả là người dùng có thể cảm thấy như thể tương tác bị trì hoãn đáng kể hoặc thậm chí bị hỏng hoàn toàn.

Việc này thường xảy ra vì luồng chính bị chặn vì JavaScript được phân tích cú pháp và biên dịch trên luồng chính. Nếu quá trình này mất quá nhiều thời gian, thì các phần tử trang tương tác có thể sẽ không phản hồi đủ nhanh với hoạt động đầu vào của người dùng. Một biện pháp khắc phục cho vấn đề này là chỉ tải JavaScript bạn cần để trang hoạt động, đồng thời trì hoãn việc tải JavaScript khác sau này thông qua một kỹ thuật có tên là phân tách mã. Mô-đun này tập trung vào nội dung sau của hai kỹ thuật này.

Giảm phân tích cú pháp và thực thi JavaScript trong quá trình khởi động thông qua việc chia tách mã

Lighthouse gửi cho bạn một cảnh báo khi quá trình thực thi JavaScript mất hơn 2 giây và không thành công khi mất hơn 3, 5 giây. Việc phân tích cú pháp và thực thi JavaScript quá mức là một vấn đề tiềm ẩn tại bất kỳ thời điểm nào trong vòng đời của trang, vì điều này có thể làm tăng độ trễ đầu vào của một lượt tương tác nếu thời điểm người dùng tương tác với trang trùng với thời điểm các tác vụ trong luồng chính chịu trách nhiệm xử lý và thực thi JavaScript đang chạy.

Hơn nữa, việc thực thi và phân tích cú pháp JavaScript quá mức đặc biệt gặp vấn đề trong lần tải trang đầu tiên, vì đây là thời điểm trong vòng đời trang mà người dùng rất có thể sẽ tương tác với trang. Trên thực tế, Tổng thời gian chặn (TBT) (một chỉ số về khả năng thích ứng khi tải) có tương quan cao với INP, cho thấy người dùng có xu hướng cao sẽ thử tương tác trong lần tải trang ban đầu.

Bài kiểm tra Lighthouse báo cáo thời gian thực thi từng tệp JavaScript mà các yêu cầu trang của bạn rất hữu ích vì có thể giúp bạn xác định chính xác tập lệnh nào có thể phù hợp để phân tách mã. Sau đó, bạn có thể tiếp tục bằng cách sử dụng công cụ đo lường trong Công cụ của Chrome cho nhà phát triển để xác định chính xác phần nào trong JavaScript của trang không được dùng đến trong quá trình tải trang.

Tách mã là một kỹ thuật hữu ích có thể làm giảm tải trọng JavaScript ban đầu của trang. Tính năng này cho phép bạn chia gói JavaScript thành hai phần:

  • JavaScript cần thiết khi tải trang và do đó, không thể tải vào bất kỳ lúc nào khác.
  • JavaScript còn lại có thể được tải vào thời điểm sau đó, thường xuyên nhất tại thời điểm người dùng tương tác với một phần tử tương tác nhất định trên trang.

Bạn có thể phân tách mã bằng cú pháp import() linh động. Cú pháp này (không giống như các phần tử <script> yêu cầu một tài nguyên JavaScript nhất định trong quá trình khởi động) tạo ra yêu cầu về một tài nguyên JavaScript sau đó trong suốt vòng đời của trang.

document.querySelectorAll('#myForm input').addEventListener('blur', async () => {
  // Get the form validation named export from the module through destructuring:
  const { validateForm } = await import('/validate-form.mjs');

  // Validate the form:
  validateForm();
}, { once: true });

Trong đoạn mã JavaScript trước đó, mô-đun validate-form.mjs chỉ được tải xuống, phân tích cú pháp và thực thi khi người dùng làm mờ bất kỳ trường nào trong <input> của biểu mẫu. Trong trường hợp này, tài nguyên JavaScript chịu trách nhiệm thúc đẩy logic xác thực của biểu mẫu chỉ liên quan đến trang khi trang đó có nhiều khả năng sẽ được sử dụng thực sự nhất.

Bạn có thể định cấu hình các trình đóng gói JavaScript như webpack, Parcel, Rollupesbuild để chia các gói JavaScript thành các phần nhỏ hơn bất cứ khi nào chúng gặp lệnh gọi import() động trong mã nguồn. Hầu hết các công cụ này sẽ tự động thực hiện việc này, nhưng đặc biệt, việc tạo bản dựng yêu cầu bạn chọn sử dụng tính năng tối ưu hoá này.

Lưu ý hữu ích về phân tách mã

Mặc dù chia tách mã là một phương pháp hiệu quả giúp giảm tranh chấp luồng chính trong lần tải trang ban đầu, nhưng bạn cần lưu ý một vài điều nếu quyết định kiểm tra mã nguồn JavaScript để biết các cơ hội phân tách mã.

Sử dụng trình phân gói nếu có thể

Các nhà phát triển thường sử dụng mô-đun JavaScript trong quá trình phát triển. Đây là một điểm cải tiến tuyệt vời về trải nghiệm nhà phát triển, giúp cải thiện khả năng đọc và bảo trì mã. Tuy nhiên, có thể có một số đặc điểm hiệu suất dưới mức tối ưu có thể xảy ra khi vận chuyển các mô-đun JavaScript sang môi trường thực tế.

Quan trọng nhất là bạn nên sử dụng trình đóng gói để xử lý và tối ưu hoá mã nguồn, bao gồm cả các mô-đun mà bạn dự định phân tách mã. Các trình đóng gói rất hiệu quả trong việc áp dụng các phương thức tối ưu hoá cho mã nguồn JavaScript, mà còn khá hiệu quả trong việc cân bằng những điểm cần cân nhắc về hiệu suất, chẳng hạn như kích thước gói với tỷ lệ nén. Hiệu quả nén sẽ tăng theo kích thước gói, nhưng các trình gói cũng cố gắng đảm bảo rằng các gói không quá lớn đến mức phải chịu các tác vụ dài do việc đánh giá tập lệnh.

Các gói cũng tránh được vấn đề vận chuyển một số lượng lớn các mô-đun chưa được nhóm qua mạng. Các cấu trúc sử dụng mô-đun JavaScript có xu hướng có các cây mô-đun lớn và phức tạp. Khi cây mô-đun không được nhóm, mỗi mô-đun đại diện cho một yêu cầu HTTP riêng và hoạt động tương tác trong ứng dụng web của bạn có thể bị trễ nếu bạn không nhóm các mô-đun. Mặc dù có thể sử dụng gợi ý về tài nguyên <link rel="modulepreload"> để tải cây mô-đun lớn càng sớm càng tốt, nhưng bạn vẫn nên ưu tiên sử dụng các gói JavaScript từ quan điểm về hiệu suất tải.

Đừng vô tình tắt tính năng biên dịch truyền trực tuyến

Công cụ JavaScript V8 của Chromium cung cấp một số biện pháp tối ưu hoá ngay từ đầu để đảm bảo rằng mã JavaScript sản xuất của bạn tải hiệu quả nhất có thể. Một trong những cách tối ưu hoá này được gọi là biên dịch truyền trực tuyến (ví dụ: phân tích cú pháp HTML tăng dần được truyền đến trình duyệt) biên dịch các đoạn JavaScript được truyền trực tuyến khi chúng đến từ mạng.

Bạn có một số cách để đảm bảo quá trình biên dịch truyền trực tuyến diễn ra cho ứng dụng web của bạn trong Chromium:

  • Chuyển đổi mã phát hành chính thức để tránh sử dụng các mô-đun JavaScript. Trình gói có thể biến đổi mã nguồn JavaScript của bạn dựa trên mục tiêu biên dịch và mục tiêu này thường dành riêng cho một môi trường nhất định. V8 sẽ áp dụng quá trình biên dịch truyền trực tuyến cho mọi mã JavaScript không sử dụng mô-đun và bạn có thể định cấu hình trình đóng gói để chuyển đổi mã mô-đun JavaScript thành cú pháp không sử dụng mô-đun JavaScript và các tính năng của chúng.
  • Nếu bạn muốn chuyển các mô-đun JavaScript vào phiên bản chính thức, hãy sử dụng phần mở rộng .mjs. Cho dù JavaScript chính thức của bạn có sử dụng mô-đun hay không, không có loại nội dung đặc biệt nào đối với JavaScript sử dụng mô-đun so với JavaScript thì không. Nếu có lo ngại về V8, bạn có thể chọn không biên dịch trực tuyến một cách hiệu quả khi gửi các mô-đun JavaScript trong phiên bản chính thức bằng tiện ích .js. Nếu bạn sử dụng tiện ích .mjs cho các mô-đun JavaScript, V8 có thể đảm bảo rằng quá trình biên dịch truyền trực tuyến cho mã JavaScript dựa trên mô-đun sẽ không bị hỏng.

Đừng để những điều cần cân nhắc này ngăn bạn sử dụng tính năng phân tách mã. Tách mã là một cách hiệu quả để giảm tải trọng JavaScript ban đầu cho người dùng. Tuy nhiên, bằng cách sử dụng trình đóng gói và biết cách duy trì hành vi biên dịch truyền trực tuyến của V8, bạn có thể đảm bảo rằng mã JavaScript sản xuất của mình hoạt động nhanh nhất có thể cho người dùng.

Bản minh hoạ tính năng nhập động

gói web

webpack đi kèm một trình bổ trợ có tên SplitChunksPlugin, cho phép bạn định cấu hình cách trình gói phân tách các tệp JavaScript. webpack nhận dạng cả câu lệnh import() động và import tĩnh. Bạn có thể sửa đổi hành vi của SplitChunksPlugin bằng cách chỉ định tuỳ chọn chunks trong cấu hình:

  • chunks: async là giá trị mặc định và đề cập đến các lệnh gọi import() động.
  • chunks: initial đề cập đến các lệnh gọi import tĩnh.
  • chunks: all bao gồm cả dữ liệu nhập import() linh động và dữ liệu nhập tĩnh, cho phép bạn chia sẻ các phần giữa lần nhập asyncinitial.

Theo mặc định, bất cứ khi nào webpack gặp phải câu lệnh import() động, nó sẽ tạo một đoạn riêng cho mô-đun đó:

/* main.js */

// An application-specific chunk required during the initial page load:
import myFunction from './my-function.js';

myFunction('Hello world!');

// If a specific condition is met, a separate chunk is downloaded on demand,
// rather than being bundled with the initial chunk:
if (condition) {
  // Assumes top-level await is available. More info:
  // https://v8.dev/features/top-level-await
  await import('/form-validation.js');
}

Cấu hình gói web mặc định cho đoạn mã trước đó dẫn đến hai đoạn riêng biệt:

  • Phần main.js (gói web được phân loại là phân đoạn initial) bao gồm mô-đun main.js./my-function.js.
  • Phân đoạn async, chỉ bao gồm form-validation.js (chứa hàm băm tệp trong tên tài nguyên nếu đã định cấu hình). Đoạn này chỉ được tải xuống nếu và khi condition chính xác.

Cấu hình này cho phép bạn trì hoãn việc tải phân đoạn form-validation.js cho đến khi thực sự cần thiết. Điều này có thể cải thiện khả năng phản hồi khi tải bằng cách giảm thời gian đánh giá tập lệnh trong lần tải trang ban đầu. Quá trình tải xuống và đánh giá tập lệnh cho phân đoạn form-validation.js xảy ra khi đáp ứng một điều kiện đã chỉ định. Trong trường hợp đó, mô-đun đã nhập động sẽ được tải xuống. Một ví dụ có thể là tình trạng polyfill chỉ được tải xuống cho một trình duyệt cụ thể, hoặc như trong ví dụ trước, mô-đun đã nhập cần thiết cho một tương tác của người dùng.

Mặt khác, việc thay đổi cấu hình SplitChunksPlugin để chỉ định chunks: initial đảm bảo rằng mã chỉ được chia trên các đoạn ban đầu. Đây là các đoạn như các đoạn được nhập tĩnh hoặc được liệt kê trong thuộc tính entry của gói web. Trong ví dụ trước, phân đoạn kết quả sẽ là sự kết hợp của form-validation.js main.js trong một tệp tập lệnh duy nhất, dẫn đến hiệu suất tải trang ban đầu có thể kém hơn.

Bạn cũng có thể định cấu hình các tuỳ chọn cho SplitChunksPlugin để phân tách các tập lệnh lớn hơn thành nhiều tập lệnh nhỏ hơn – ví dụ: bằng cách sử dụng tuỳ chọn maxSize để hướng dẫn gói web chia các phần thành các tệp riêng biệt nếu các tập lệnh đó vượt quá nội dung do maxSize chỉ định. Việc chia các tệp tập lệnh lớn thành các tệp nhỏ hơn có thể cải thiện khả năng phản hồi khi tải, vì trong một số trường hợp, công việc đánh giá tập lệnh nặng về CPU sẽ được chia thành các tác vụ nhỏ hơn, ít có khả năng chặn luồng chính trong khoảng thời gian dài hơn.

Ngoài ra, việc tạo tệp JavaScript lớn hơn cũng đồng nghĩa với việc tập lệnh có nhiều khả năng bị vô hiệu hoá bộ nhớ đệm. Ví dụ: nếu bạn gửi một tập lệnh rất lớn có cả mã khung và mã ứng dụng bên thứ nhất, thì toàn bộ gói có thể bị vô hiệu hoá nếu chỉ cập nhật khung chứ không có nội dung nào khác trong tài nguyên đi kèm.

Mặt khác, các tệp tập lệnh nhỏ hơn làm tăng khả năng khách truy cập quay lại truy xuất tài nguyên từ bộ nhớ đệm, nhờ đó, trang sẽ tải nhanh hơn trong những lần truy cập lặp lại. Tuy nhiên, các tệp nhỏ hơn được hưởng lợi ít hơn từ việc nén so với các tệp lớn hơn và có thể làm tăng thời gian tải hai chiều trên mạng khi tải trang với bộ nhớ đệm của trình duyệt chưa được bảo vệ. Bạn phải chú ý để cân bằng giữa hiệu suất lưu vào bộ nhớ đệm, hiệu quả nén và thời gian đánh giá tập lệnh.

bản minh hoạ gói web

bản minh hoạ gói web SplitChunksPlugin.

Kiểm tra kiến thức

Loại câu lệnh import nào được dùng khi phân tách mã?

import() động.
Chính xác!
import tĩnh.
Hãy thử lại.

Loại câu lệnh import nào phải nằm ở đầu mô-đun JavaScript và không ở vị trí nào khác?

import() động.
Hãy thử lại.
import tĩnh.
Chính xác!

Khi sử dụng SplitChunksPlugin trong gói web, sự khác biệt giữa phân đoạn async và phân đoạn initial là gì?

Các phân đoạn async được tải bằng các phân đoạn import() linh động và các phân đoạn initial được tải bằng import tĩnh.
Chính xác!
Các phân đoạn async được tải bằng các phân đoạn import tĩnh và các phân đoạn initial được tải bằng import() linh động.
Hãy thử lại.

Tiếp theo: Tải từng phần hình ảnh và phần tử <iframe>

Mặc dù đây có xu hướng là một loại tài nguyên khá tốn kém, nhưng JavaScript không phải là loại tài nguyên duy nhất bạn có thể trì hoãn việc tải. Các phần tử hình ảnh và <iframe> là các tài nguyên có thể tốn kém khi hoạt động theo cách riêng. Tương tự như JavaScript, bạn có thể trì hoãn việc tải hình ảnh và phần tử <iframe> bằng cách tải từng phần. Việc này được giải thích trong học phần tiếp theo của khoá học này.