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 chia JavaScript thành các đoạn nhỏ hơn và chỉ tải xuống những gì 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 khi tải của trang. Nhờ đó, Tương tác đến lần hiển thị tiếp theo (INP) của trang có thể được cải thiện.

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ử trên trang đều hiển thị vì chúng là một phần của HTML ban đầu của trang và được tạo kiểu bằng CSS. Tuy nhiên, vì JavaScript cần thiết để 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ể đang 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ể hoạt động tương tác bị chậm trễ đáng kể, hoặc thậm chí bị gián đoạn hoàn toàn.

Điều này thường xảy ra do 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 diễn ra quá lâu, các phần tử trang tương tác có thể 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 mà bạn cần để trang hoạt động, đồng thời trì hoãn việc tải JavaScript khác vào thời điểm sau thông qua một kỹ thuật được gọi là phân tách mã. Mô-đun này tập trung vào kỹ thuật thứ hai trong hai kỹ thuật này.

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

Lighthouse sẽ đưa ra cảnh báo khi quá trình thực thi JavaScript mất hơn 2 giây và sẽ thất bại 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ì vấn đề này có khả năng 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ụ của 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ây ra vấn đề trong quá trình tải trang ban đầu, vì đây là thời điểm trong vòng đời của trang mà người dùng có nhiều khả năng 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 phản hồi khi tải – tương quan cao với INP, cho thấy người dùng có xu hướng cao là cố gắng tương tác trong quá trình tải trang ban đầu.

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

Phân tách mã là một kỹ thuật hữu ích có thể giảm tải trọng JavaScript ban đầu của trang. Tính năng này cho phép bạn chia một 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ỳ thời điểm nào khác.
  • JavaScript còn lại có thể được tải vào thời điểm sau đó, thường là vào 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 chia mã bằng cách sử dụ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) sẽ đưa ra yêu cầu về một tài nguyên JavaScript vào thời điểm muộn hơn trong 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 <input> nào của biểu mẫu. Trong trường hợp này, tài nguyên JavaScript chịu trách nhiệm điều khiển logic xác thực của biểu mẫu chỉ liên quan đến trang khi có khả năng được sử dụng thực tế 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 đoạn nhỏ hơn bất cứ khi nào chúng gặp phải một lệnh gọi import() động trong mã nguồn của bạn. Hầu hết các công cụ này đều tự động thực hiện việc này, nhưng đặc biệt là esbuild yêu cầu bạn chọn sử dụng chế độ tối ưu hoá này.

Các ghi chú hữu ích về việc phân chia mã

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

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

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

Quan trọng nhất là bạn nên sử dụng một 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ã. Trình đóng gói rất hiệu quả không chỉ trong việc áp dụng các hoạt động tối ưu hoá cho mã nguồn JavaScript mà còn khá hiệu quả trong việc cân bằng các yếu tố cần cân nhắc về hiệu suất, chẳng hạn như kích thước gói so với tỷ lệ nén. Hiệu quả nén tăng lên khi kích thước gói tăng lên, nhưng các trình đóng gói cũng cố gắng đảm bảo rằng các gói không quá lớn để chúng không phát sinh các tác vụ dài do quá trình đánh giá tập lệnh.

Trình đóng 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 đóng gói qua mạng. Các cấu trúc sử dụng mô-đun JavaScript thường có các cây mô-đun lớn và phức tạp. Khi các cây mô-đun không được kết hợp, mỗi mô-đun sẽ đại diện cho một yêu cầu HTTP riêng biệt và tính tương tác trong ứng dụng web của bạn có thể bị trì hoãn nếu bạn không kết hợp các mô-đun. Mặc dù bạn có thể dùng gợi ý về tài nguyên <link rel="modulepreload"> để tải các cây mô-đun lớn sớm nhất có thể, nhưng các gói JavaScript vẫn được ưu tiên hơn về hiệu suất tải.

Khô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ố chế độ tối ưu hoá ngay khi xuất xưởng để đảm bảo mã JavaScript sản xuất của bạn tải hiệu quả nhất có thể. Một trong những hoạt động tối ưu hoá này được gọi là biên dịch trực tuyến. Giống như việc phân tích cú pháp gia tăng của HTML được truyền trực tuyến đến trình duyệt, hoạt động này sẽ 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ó thể thực hiện theo 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ã sản xuất để tránh sử dụng các mô-đun JavaScript. Trình đóng gói có thể chuyể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 thường dành riêng cho một môi trường nhất định. V8 sẽ áp dụng quy 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 kết hợp để chuyển đổi mã mô-đun JavaScript thành một cú pháp không sử dụng mô-đun JavaScript và các tính năng của mô-đun này.
  • Nếu bạn muốn chuyển các mô-đun JavaScript sang phiên bản phát hành công khai, hãy sử dụng tiện ích .mjs. Cho dù JavaScript trong bản phát hành của bạn có sử dụng mô-đun hay không, thì cũng không có loại nội dung đặc biệt nào cho JavaScript sử dụng mô-đun so với JavaScript không sử dụng mô-đun. Đối với V8, bạn sẽ chọn không sử dụng tính năng biên dịch truyền trực tuyến khi phân phối các mô-đun JavaScript trong quá trình sản xuất bằng cách sử dụ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 không bị gián đoạn.

Đừng để những điều này khiến bạn không sử dụng tính năng phân chia mã. Phân 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, nhưng bằng cách sử dụng một 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 nhanh nhất có thể cho người dùng.

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

webpack

webpack đi kèm với một trình bổ trợ có tên là SplitChunksPlugin, cho phép bạn định cấu hình cách trình kết hợp chia các tệp JavaScript. webpack nhận dạng cả câu lệnh động import() và câu lệnh tĩnh import. 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 của tuỳ chọn này:

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

Theo mặc định, bất cứ khi nào webpack gặp phải một 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 webpack mặc định cho đoạn mã trước đó sẽ tạo ra 2 đoạn riêng biệt:

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

Cấu hình này cho phép bạn trì hoãn việc tải đoạn form-validation.js cho đến khi thực sự cần. Điều này có thể cải thiện khả năng phản hồi 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 đoạn form-validation.js sẽ diễn ra khi một điều kiện cụ thể được đáp ứng. Trong trường hợp đó, mô-đun được nhập động sẽ được tải xuống. Một ví dụ có thể là điều kiện mà 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 là cần thiết cho một lượ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 thành các đoạn ban đầu. Đây là những đoạn mã như những đoạn mã được nhập tĩnh hoặc được liệt kê trong thuộc tính entry của webpack. Nhìn vào ví dụ trước, khối 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 lựa chọn cho SplitChunksPlugin để 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 lựa chọn maxSize để hướng dẫn webpack chia các đoạn mã thành các tệp riêng biệt nếu chúng vượt quá những gì được chỉ định bởi maxSize. 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 sử dụng nhiều CPU sẽ được chia thành các tác vụ nhỏ hơn. Các tác vụ này ít có khả năng chặn luồng chính trong thời gian dài hơn.

Ngoài ra, việc tạo các tệp JavaScript lớn hơn cũng có nghĩa là cá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 vận chuyển một tập lệnh rất lớn có cả mã ứng dụng của khung và bên thứ nhất, thì toàn bộ gói có thể bị vô hiệu hoá nếu chỉ khung được cập nhật, nhưng không có gì khác trong tài nguyên được đóng gói.

Mặt khác, các tệp tập lệnh nhỏ hơn sẽ 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, dẫn đến việc tải trang nhanh hơn trong các lượt truy cập lặp lại. Tuy nhiên, các tệp nhỏ hơn sẽ ít được hưởng lợi từ việc nén hơn các tệp lớn hơn và có thể làm tăng thời gian khứ hồi của mạng khi tải trang bằng bộ nhớ đệm trình duyệt chưa được chuẩn bị. Bạn phải chú ý cân bằng giữa hiệu quả lưu vào bộ nhớ đệm, hiệu quả nén và thời gian đánh giá tập lệnh.

webpack demo

Bản minh hoạ SplitChunksPlugin webpack.

Kiểm tra kiến thức của bạn

Loại câu lệnh import nào được dùng khi thực hiện việc phân chia mã?

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

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

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

Khi sử dụng SplitChunksPlugin trong webpack, sự khác biệt giữa một đoạn async và một đoạn initial là gì?

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

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

Mặc dù 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 mà bạn có thể trì hoãn việc tải. Các phần tử hình ảnh và <iframe> có thể là những tài nguyên tốn kém theo đúng nghĩa của chú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 chúng một cách trì hoãn. Việc này sẽ được giải thích trong mô-đun tiếp theo của khoá học này.