Đánh giá tập lệnh và các tác vụ dài

Khi tải tập lệnh, trình duyệt cần có thời gian để đánh giá các tập lệnh đó trước khi thực thi, điều này có thể khiến các tác vụ mất nhiều thời gian. Tìm hiểu cách hoạt động của việc đánh giá tập lệnh và những việc bạn có thể làm để ngăn việc đánh giá tập lệnh gây ra các thao tác dài trong quá trình tải trang.

Khi nói đến việc tối ưu hoá Lượt tương tác với thời điểm hiển thị tiếp theo (INP), bạn nên tự tối ưu hoá các lượt tương tác. Ví dụ: trong hướng dẫn về thao tác tối ưu hoá cho các thao tác dài, bạn sẽ thấy các kỹ thuật như tạo bằng setTimeout, isInputPending, v.v. Những kỹ thuật này rất có lợi vì chúng tạo điều kiện cho luồng chính một số chỗ thở bằng cách tránh các nhiệm vụ dài, tạo điều kiện cho các hoạt động tương tác và hoạt động khác chạy sớm hơn, thay vì phải đợi một nhiệm vụ dài hạn.

Tuy nhiên, còn các tác vụ dài do chính việc tải tập lệnh thì sao? Những tác vụ này có thể ảnh hưởng đến tương tác của người dùng và ảnh hưởng đến INP của trang trong quá trình tải. Hướng dẫn này sẽ tìm hiểu cách trình duyệt xử lý các tác vụ bắt đầu bằng việc đánh giá tập lệnh và xem xét những việc bạn có thể làm để chia nhỏ công việc đánh giá tập lệnh sao cho luồng chính của bạn có thể phản hồi nhanh hơn đối với hoạt động đầu vào của người dùng trong khi trang đang tải.

Đánh giá tập lệnh là gì?

Nếu đã phân tích tài nguyên cho một ứng dụng phân phối nhiều JavaScript, thì có thể bạn đã thấy các tác vụ dài mà thủ phạm có nhãn là Đánh giá tập lệnh.

Quá trình đánh giá tập lệnh hoạt động như trực quan trong trình phân tích hiệu suất của Công cụ của Chrome cho nhà phát triển. Tác vụ này gây ra một tác vụ mất nhiều thời gian trong quá trình khởi động, khiến luồng chính không thể phản hồi tương tác của người dùng.
Quá trình đánh giá tập lệnh hoạt động như minh hoạ trong trình phân tích hiệu suất trong Công cụ của Chrome cho nhà phát triển. Trong trường hợp này, tác vụ đó đủ để tạo ra một tác vụ dài khiến luồng chính thực hiện công việc khác, bao gồm cả các tác vụ thúc đẩy tương tác của người dùng.

Việc đánh giá tập lệnh là một phần cần thiết khi thực thi JavaScript trên trình duyệt, vì JavaScript được biên dịch đúng lúc trước khi thực thi. Khi một tập lệnh được đánh giá, trước tiên, tập lệnh đó sẽ được phân tích cú pháp để tìm lỗi. Nếu trình phân tích cú pháp không tìm thấy lỗi, tập lệnh sẽ được biên dịch thành mã byte và sau đó có thể tiếp tục thực thi.

Mặc dù cần thiết nhưng việc đánh giá tập lệnh có thể gặp vấn đề vì người dùng có thể cố gắng tương tác với một trang ngay sau khi trang hiển thị lần đầu. Tuy nhiên, việc một trang đã hiển thị không có nghĩa là trang đó đã tải xong. Các tương tác diễn ra trong khi tải có thể bị trì hoãn vì trang đang bận đánh giá tập lệnh. Mặc dù không có gì đảm bảo rằng tương tác mong muốn có thể diễn ra vào thời điểm này — vì tập lệnh chịu trách nhiệm về hoạt động đó có thể chưa tải — có thể có các tương tác phụ thuộc vào JavaScript đã sẵn sàng hoặc tính tương tác hoàn toàn không phụ thuộc vào JavaScript.

Mối quan hệ giữa tập lệnh và các nhiệm vụ đánh giá tập lệnh

Cách bắt đầu các tác vụ chịu trách nhiệm đánh giá tập lệnh phụ thuộc vào việc tập lệnh bạn đang tải được tải thông qua phần tử <script> thông thường hay tập lệnh là một mô-đun được tải bằng type=module. Vì các trình duyệt có xu hướng xử lý mọi thứ theo cách khác nhau, nên cách các công cụ trình duyệt chính xử lý việc đánh giá tập lệnh sẽ được tác động vào nơi hành vi đánh giá tập lệnh trên chúng khác nhau.

Đang tải tập lệnh bằng phần tử <script>

Số lượng tác vụ được gửi đi để đánh giá tập lệnh thường có mối quan hệ trực tiếp với số lượng phần tử <script> trên một trang. Mỗi phần tử <script> khởi động một tác vụ nhằm đánh giá tập lệnh được yêu cầu để tập lệnh đó có thể được phân tích cú pháp, biên dịch và thực thi. Trường hợp này áp dụng cho các trình duyệt dựa trên Chromium, Safari Firefox.

Vì sao điều này quan trọng? Giả sử bạn đang sử dụng trình tạo gói để quản lý tập lệnh sản xuất và bạn đã định cấu hình trình tạo gói để nhóm mọi thứ mà trang của bạn cần để chạy vào một tập lệnh duy nhất. Trong trường hợp này đối với trang web của bạn, có thể bạn sẽ nhận được một tác vụ duy nhất để đánh giá tập lệnh đó. Đây có phải là điều không hay không? Không cần thiết — trừ khi tập lệnh đó rất lớn.

Bạn có thể chia nhỏ công việc đánh giá tập lệnh bằng cách tránh tải các phần lớn JavaScript và tải nhiều tập lệnh riêng lẻ hơn, sử dụng các phần tử <script> bổ sung.

Mặc dù bạn phải luôn cố gắng tải ít JavaScript nhất có thể trong quá trình tải trang, nhưng việc chia nhỏ các tập lệnh giúp đảm bảo rằng, thay vì một tác vụ lớn có thể chặn chuỗi chính, bạn sẽ có nhiều tác vụ nhỏ hơn mà không chặn chuỗi chính nào cả—hoặc ít nhất là ít hơn so với những gì bạn đã bắt đầu.

Nhiều tác vụ liên quan đến việc đánh giá tập lệnh được thể hiện trực quan trong trình phân tích hiệu suất của Công cụ của Chrome cho nhà phát triển. Vì nhiều tập lệnh nhỏ hơn được tải thay vì ít tập lệnh lớn hơn, nên các tác vụ ít có khả năng trở thành các tác vụ dài, cho phép luồng chính phản hồi hoạt động đầu vào của người dùng nhanh hơn.
Nhiều tác vụ được tạo ra để đánh giá tập lệnh do nhiều phần tử <script> có trong HTML của trang. Bạn nên gửi một gói tập lệnh lớn cho người dùng vì như vậy sẽ có nhiều khả năng chặn luồng chính.

Bạn có thể coi việc chia nhỏ tác vụ để đánh giá tập lệnh tương tự như việc tạo tác vụ trong các lệnh gọi lại sự kiện chạy trong một tương tác. Tuy nhiên, bằng việc đánh giá tập lệnh, cơ chế lợi nhuận chia nhỏ JavaScript bạn tải thành nhiều tập lệnh nhỏ hơn, thay vì một số lượng ít tập lệnh lớn hơn có nhiều khả năng chặn chuỗi chính.

Đang tải tập lệnh bằng phần tử <script> và thuộc tính type=module

Giờ đây, bạn có thể tải các mô-đun ES nguyên gốc trong trình duyệt bằng thuộc tính type=module trên phần tử <script>. Phương pháp tải tập lệnh này mang lại một số lợi ích cho trải nghiệm của nhà phát triển, chẳng hạn như không phải chuyển đổi mã để sử dụng trong phiên bản chính thức – đặc biệt là khi sử dụng kết hợp với bản đồ nhập. Tuy nhiên, việc tải tập lệnh theo cách này sẽ lên lịch các tác vụ khác nhau giữa các trình duyệt.

Trình duyệt dựa trên Chromium

Trong các trình duyệt như Chrome (hoặc các trình duyệt bắt nguồn từ Chrome) việc tải các mô-đun ES bằng thuộc tính type=module sẽ tạo ra nhiều loại tác vụ so với bạn thường thấy khi không sử dụng type=module. Ví dụ: một tác vụ cho từng tập lệnh mô-đun sẽ chạy có liên quan đến hoạt động được gắn nhãn Mô-đun biên dịch.

Công việc biên dịch mô-đun trong nhiều tác vụ như được trực quan hoá trong Công cụ của Chrome cho nhà phát triển.
Hành vi tải mô-đun trong các trình duyệt dựa trên Chromium. Mỗi tập lệnh mô-đun sẽ tạo một lệnh gọi Compile mô-đun (Mô-đun biên dịch) để biên dịch nội dung trước khi đánh giá.

Sau khi các mô-đun được biên dịch, mọi mã chạy sau đó trong các mô-đun đó sẽ kích hoạt hoạt động được gắn nhãn Đánh giá mô-đun.

Đánh giá đúng thời điểm về một mô-đun như trực quan hoá trong bảng điều khiển hiệu suất của Công cụ của Chrome cho nhà phát triển.
Khi mã trong một mô-đun chạy, mô-đun đó sẽ được đánh giá đúng lúc.

Ảnh hưởng ở đây (ít nhất là trong Chrome và các trình duyệt liên quan) là các bước biên dịch bị chia nhỏ khi sử dụng các mô-đun ES. Đây là một lợi thế rõ ràng khi quản lý các tác vụ dài; tuy nhiên, công việc đánh giá mô-đun kết quả có nghĩa là bạn vẫn phải chịu một số chi phí không tránh được. Mặc dù bạn nên cố gắng gửi ít JavaScript nhất có thể, nhưng việc sử dụng mô-đun ES (bất kể trình duyệt là gì) đều mang lại những lợi ích sau:

  • Tất cả mã mô-đun sẽ tự động chạy ở chế độ nghiêm ngặt, cho phép tối ưu hoá tiềm năng mà các công cụ JavaScript không thể thực hiện được trong ngữ cảnh không nghiêm ngặt.
  • Những tập lệnh được tải bằng type=module được xem như bị trì hoãn theo mặc định. Bạn có thể sử dụng thuộc tính async trên các tập lệnh được tải bằng type=module để thay đổi hành vi này.

Safari và Firefox

Khi các mô-đun được tải trong Safari và Firefox, mỗi mô-đun sẽ được đánh giá trong một tác vụ riêng. Điều này có nghĩa là về mặt lý thuyết, bạn có thể tải một mô-đun cấp cao nhất chỉ bao gồm các câu lệnh import tĩnh cho các mô-đun khác, đồng thời mỗi mô-đun được tải sẽ phải chịu một yêu cầu mạng và tác vụ riêng để đánh giá mô-đun đó.

Đang tải tập lệnh bằng import() động

import() động là một phương thức khác để tải tập lệnh. Không giống như các câu lệnh import tĩnh bắt buộc phải nằm ở đầu một mô-đun ES, lệnh gọi import() động có thể xuất hiện ở vị trí bất kỳ trong một tập lệnh để tải một đoạn JavaScript theo yêu cầu. Kỹ thuật này được gọi là phân tách mã.

Để cải thiện INP, import() động có hai ưu điểm:

  1. Các mô-đun bị trì hoãn để tải sau giúp giảm tranh chấp luồng chính trong quá trình khởi động bằng cách giảm lượng JavaScript được tải tại thời điểm đó. Việc này sẽ giải phóng luồng chính để có thể phản hồi nhanh hơn với các hoạt động tương tác của người dùng.
  2. Khi các lệnh gọi import() động được thực hiện, mỗi lệnh gọi sẽ phân tách hiệu quả hoạt động biên dịch và đánh giá từng mô-đun cho tác vụ riêng của mô-đun đó. Tất nhiên, một import() động tải một mô-đun rất lớn sẽ khởi động một tác vụ đánh giá tập lệnh khá lớn và có thể ảnh hưởng đến khả năng phản hồi hoạt động đầu vào của người dùng của luồng chính nếu hoạt động tương tác xảy ra cùng lúc với lệnh gọi import() động. Do đó, bạn vẫn cần phải tải ít JavaScript nhất có thể.

Các lệnh gọi import() động hoạt động tương tự nhau trong tất cả các công cụ trình duyệt chính: các tác vụ đánh giá tập lệnh dẫn đến sẽ giống với số lượng mô-đun được nhập động.

Tải tập lệnh trong một trình chạy web

Trình thực thi web là một trường hợp sử dụng JavaScript đặc biệt. Trình thực thi web được đăng ký trên luồng chính, sau đó mã trong worker này sẽ chạy trên luồng riêng. Điều này vô cùng có lợi vì trong khi mã đăng ký trình thực thi web chạy trên luồng chính, thì mã trong trình thực thi web thì không. Điều này làm giảm tình trạng nghẽn luồng chính và có thể giúp luồng chính phản hồi nhanh hơn với các hoạt động tương tác của người dùng.

Ngoài việc giảm công việc của luồng chính, chính trình thực thi cũng có thể tải các tập lệnh bên ngoài để dùng trong ngữ cảnh worker, thông qua câu lệnh importScripts hoặc import tĩnh trong các trình duyệt hỗ trợ trình chạy mô-đun. Kết quả là bất kỳ tập lệnh nào do trình thực thi web yêu cầu đều được đánh giá ngoài luồng chính.

Đánh đổi và cân nhắc

Mặc dù việc chia nhỏ các tập lệnh của bạn thành các tệp riêng biệt, nhỏ hơn giúp hạn chế tác vụ dài thay vì tải ít tệp hơn và tệp lớn hơn nhiều, nhưng điều quan trọng cần phải tính đến một số điều khi quyết định cách chia nhỏ tập lệnh.

Hiệu quả nén

Nén là một yếu tố khi chia nhỏ tập lệnh. Khi tập lệnh nhỏ hơn, việc nén sẽ trở nên kém hiệu quả hơn một chút. Các tập lệnh lớn hơn sẽ hưởng nhiều lợi ích hơn từ việc nén. Mặc dù việc tăng hiệu quả nén sẽ giúp duy trì thời gian tải cho tập lệnh ở mức thấp nhất có thể, nhưng đó là một chút của hoạt động cân bằng để đảm bảo rằng bạn chia tập lệnh thành các phần đủ nhỏ hơn để hỗ trợ tương tác tốt hơn trong quá trình khởi động.

Trình gói là công cụ lý tưởng để quản lý kích thước đầu ra cho các tập lệnh mà trang web của bạn phụ thuộc:

  • Trong trường hợp liên quan đến gói web, trình bổ trợ SplitChunksPlugin của gói web có thể giúp ích. Tham khảo tài liệu SplitChunksPlugin để biết các lựa chọn mà bạn có thể thiết lập để giúp quản lý kích thước thành phần.
  • Đối với những trình đóng gói khác như Rollupesbuild, bạn có thể quản lý kích thước tệp tập lệnh bằng cách sử dụng các lệnh gọi import() động trong mã của mình. Các trình đóng gói này cũng như gói web sẽ tự động chia thành phần được nhập linh động thành tệp riêng, nhờ đó tránh được kích thước gói ban đầu lớn hơn.

Vô hiệu hoá bộ nhớ đệm

Việc vô hiệu hoá bộ nhớ đệm đóng một vai trò lớn đối với tốc độ tải của một trang trong các lần truy cập lặp lại. Khi gửi các gói tập lệnh lớn, nguyên khối, bạn sẽ gặp bất lợi khi nói đến việc lưu vào bộ nhớ đệm của trình duyệt. Điều này là do khi bạn cập nhật mã của bên thứ nhất (thông qua việc cập nhật gói hoặc sửa lỗi vận chuyển), thì toàn bộ gói sẽ không hợp lệ và phải được tải xuống lại.

Bằng cách chia nhỏ các tập lệnh, bạn không chỉ chia nhỏ công việc đánh giá tập lệnh qua các tác vụ nhỏ hơn, bạn còn tăng khả năng khách truy cập cũ sẽ lấy thêm tập lệnh từ bộ nhớ đệm của trình duyệt thay vì từ mạng. Việc này sẽ giúp trang tải tổng thể nhanh hơn.

Các mô-đun lồng nhau và hiệu suất tải

Nếu đang vận chuyển mô-đun ES trong phiên bản chính thức và tải các mô-đun đó bằng thuộc tính type=module, thì bạn cần lưu ý rằng việc lồng mô-đun có thể ảnh hưởng đến thời gian khởi động như thế nào. Lồng ghép mô-đun là khi một mô-đun ES nhập tĩnh một mô-đun ES khác và nhập tĩnh một mô-đun ES khác:

// a.js
import {b} from './b.js';

// b.js
import {c} from './c.js';

Nếu các mô-đun ES không được nhóm với nhau, thì mã trước đó sẽ dẫn đến một chuỗi yêu cầu mạng: khi a.js được yêu cầu qua phần tử <script>, một yêu cầu mạng khác sẽ được gửi cho b.js, sau đó liên quan đến một yêu cầu khác cho c.js. Một cách để tránh tình trạng này là sử dụng trình đóng gói, nhưng hãy đảm bảo bạn đang định cấu hình trình đóng gói để chia nhỏ các tập lệnh nhằm chia nhỏ công việc đánh giá tập lệnh.

Nếu bạn không muốn sử dụng trình đóng gói, thì có một cách khác để tránh các lệnh gọi mô-đun lồng nhau là sử dụng gợi ý về tài nguyên modulepreload. Gợi ý này sẽ tải trước các mô-đun ES để tránh các chuỗi yêu cầu mạng.

Kết luận

Việc tối ưu hoá việc đánh giá tập lệnh trong trình duyệt chắc chắn là một công việc khó khăn. Phương pháp này phụ thuộc vào các yêu cầu và hạn chế của trang web của bạn. Tuy nhiên, bằng cách chia nhỏ các tập lệnh, bạn sẽ trải rộng công việc đánh giá tập lệnh sang nhiều tác vụ nhỏ hơn, từ đó mang lại cho luồng chính khả năng xử lý hoạt động tương tác của người dùng hiệu quả hơn, thay vì chặn luồng chính.

Tóm lại, sau đây là một số việc bạn có thể làm để chia nhỏ các tác vụ đánh giá tập lệnh lớn:

  • Khi tải tập lệnh bằng phần tử <script> không có thuộc tính type=module, hãy tránh tải các tập lệnh quá lớn, vì các tập lệnh này sẽ khởi động các tác vụ đánh giá tập lệnh tốn nhiều tài nguyên chặn luồng chính. Mở rộng tập lệnh trên nhiều phần tử <script> hơn để phân chia công việc này.
  • Việc sử dụng thuộc tính type=module để tải các mô-đun ES vốn có trong trình duyệt sẽ kích hoạt từng tác vụ riêng lẻ để đánh giá từng tập lệnh mô-đun riêng.
  • Giảm kích thước của các gói ban đầu bằng cách sử dụng lệnh gọi import() động. Điều này cũng có tác dụng trong các trình đóng gói, vì các trình đóng gói sẽ coi mỗi mô-đun được nhập động là một "điểm phân tách", dẫn đến một tập lệnh riêng được tạo cho mỗi mô-đun được nhập động.
  • Hãy nhớ cân nhắc các yếu tố đánh đổi như hiệu quả nén và việc vô hiệu hoá bộ nhớ đệm. Các tập lệnh lớn hơn sẽ nén tốt hơn, nhưng có nhiều khả năng kéo theo nhiều công việc đánh giá tập lệnh tốn kém hơn trong ít tác vụ hơn và dẫn đến vô hiệu hoá bộ nhớ đệm của trình duyệt, dẫn đến hiệu quả lưu vào bộ nhớ đệm tổng thể thấp hơn.
  • Nếu sử dụng nguyên gốc các mô-đun ES mà không cần gói, hãy sử dụng gợi ý về tài nguyên modulepreload để tối ưu hoá việc tải các mô-đun đó trong quá trình khởi động.
  • Như thường lệ, hãy vận chuyển ít JavaScript nhất có thể.

Chắc chắn đây là hành động cân bằng. Tuy nhiên, bằng cách chia nhỏ các tập lệnh và giảm tải trọng ban đầu thông qua import() động, bạn có thể đạt được hiệu suất khởi động tốt hơn và đáp ứng tốt hơn các hoạt động tương tác của người dùng trong giai đoạn khởi động quan trọng đó. Điều này sẽ giúp bạn đạt điểm cao hơn về chỉ số INP, nhờ đó mang lại trải nghiệm người dùng tốt hơn.

Hình ảnh chính từ Unsplash, của Markus Spiske.