Đá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 thời gian để đánh giá các tập lệnh đó trước khi thực thi, điều này có thể gây ra các tác vụ dài. Tìm hiểu cách hoạt động của quy trình đánh giá tập lệnh và những việc bạn có thể làm để ngăn việc này gây ra các tác vụ lâu trong quá trình tải trang.

Khi nói đến việc tối ưu hoá Lượt tương tác đến nội dung hiển thị tiếp theo (INP), hầu hết các lời khuyên bạn sẽ gặp phải là tối ưu hoá chính các lượt tương tác. Ví dụ: trong hướng dẫn tối ưu hoá tác vụ dài, các kỹ thuật như trả về bằng setTimeout và các kỹ thuật khác được thảo luận. Những kỹ thuật này rất hữu ích vì giúp luồng chính có thời gian nghỉ ngơi bằng cách tránh các tác vụ dài. Điều này có thể tạo ra nhiều cơ hội hơn để 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 chờ một tác vụ dài.

Tuy nhiên, còn những tác vụ dài xuất phát từ việc tự tải tập lệnh thì sao? Những tác vụ này có thể cản trở hoạt động tương tác của người dùng và ảnh hưởng đến INP của trang trong khi tải. Hướng dẫn này sẽ khám phá cách trình duyệt xử lý các tác vụ do hoạt động đánh giá tập lệnh khởi động, đồng thời 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 để luồng chính có thể phản hồi tốt 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 một ứng dụng vận chuyển nhiều JavaScript, bạn có thể đã thấy các tác vụ dài mà thủ phạm được gắn nhãn là Evaluate Script (Đánh giá tập lệnh).

Hoạt động đánh giá tập lệnh được hiển thị 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. Công việc này gây ra một tác vụ dài trong quá trình khởi động, chặn khả năng của luồng chính để phản hồi các lượt tương tác của người dùng.
Công việc đánh giá tập lệnh như hiển thị 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, công việc này đủ để gây ra một tác vụ dài hạn khiến luồng chính không thể thực hiện công việc khác, bao gồm cả các tác vụ thúc đẩy hoạt động 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 để thực thi JavaScript trong trình duyệt, vì JavaScript được biên dịch vừa kịp thời trước khi thực thi. Khi đánh giá một tập lệnh, 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à 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 lượt tương tác diễn ra trong khi tải có thể bị trì hoãn vì trang đang bận đánh giá các tập lệnh. Mặc dù không thể đảm bảo rằng một lượt tương tác có thể diễn ra tại thời điểm này (vì tập lệnh chịu trách nhiệm về lượt tương tác đó có thể chưa tải xong), nhưng có thể có các lượt tương tác phụ thuộc vào JavaScript đã sẵn sàng hoặc tính tương tác không phụ thuộc vào JavaScript.

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

Cách các tác vụ chịu trách nhiệm đánh giá tập lệnh được khởi động phụ thuộc vào việc tập lệnh bạn đang tải có được tải bằng phần tử <script> thông thường hay không hoặc liệu tập lệnh có phải là mô-đun được tải bằng type=module hay không. 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 đề cập đến khi hành vi đánh giá tập lệnh trên các trình duyệt đó khác nhau.

Tập lệnh được tải bằng phần tử <script>

Số lượng tác vụ được gử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> sẽ bắt đầu một tác vụ để đánh giá tập lệnh được yêu cầu để có thể phân tích cú pháp, biên dịch và thực thi tập lệnh đó. Đây là trường hợp đối với các trình duyệt dựa trên Chromium, Safari Firefox.

Tầm quan trọng của điều này Giả sử bạn đang sử dụng trình đóng gói để quản lý các tập lệnh phát hành chính thức và bạn đã định cấu hình trình đóng gói đó để đóng gói mọi thứ mà trang của bạn cần để chạy vào một tập lệnh duy nhất. Nếu trang web của bạn gặp trường hợp này, bạn có thể dự kiến sẽ có một tác vụ duy nhất được gửi để đánh giá tập lệnh đó. Điều này có phải là điều xấu không? Không nhất thiết, trừ phi tập lệnh đó quá 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 đoạn JavaScript lớn và tải nhiều tập lệnh nhỏ hơn, riêng lẻ hơn bằng cách sử dụng các phần tử <script> bổ sung.

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

Nhiều tác vụ liên quan đến việc đánh giá tập lệnh được hiển thị trong trình phân tích hiệu suất của Chrome DevTools. 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 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 để đánh giá tập lệnh do nhiều phần tử <script> có trong HTML của trang. Điều này tốt hơn so với việc gửi một gói tập lệnh lớn đến người dùng, vì điều này có nhiều khả năng chặn luồng chính.

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

Các tập lệnh được tải 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 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 về trải nghiệm cho nhà phát triển, chẳng hạn như không phải chuyển đổi mã để sử dụng trong sản xuất, đặ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 tuỳ theo 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ừ trình duyệt này, 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ụ khác nhau so với những tác vụ mà bạn thường thấy khi không sử dụng type=module. Ví dụ: một tác vụ cho mỗi tập lệnh mô-đun sẽ chạy liên quan đến hoạt động được gắn nhãn là Compile module (Biên dịch mô-đun).

Công việc biên dịch mô-đun trong nhiều tác vụ như được minh hoạ trong Chrome DevTools.
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 module (Biên dịch mô-đun) để biên dịch nội dung của các tập lệnh đó trước khi đánh giá.

Sau khi các mô-đun được biên dịch, mọi mã chạy trong đó sau đó sẽ bắt đầu hoạt động được gắn nhãn là Evaluate module (Đánh giá mô-đun).

Đánh giá đúng thời điểm một mô-đun như được minh 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.

Hiệu ứng ở đây (ít nhất là trong Chrome và các trình duyệt có 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 về việc quản lý các tác vụ dài; tuy nhiên, công việc đánh giá mô-đun thu được vẫn có nghĩa là bạn đang phải chịu một số chi phí không thể tránh khỏi. 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 các mô-đun ES (bất kể trình duyệt) sẽ mang lại những lợi ích sau:

  • Tất cả mã mô-đun được tự động chạy ở chế độ nghiêm ngặt, cho phép các công cụ JavaScript tối ưu hoá tiềm năng mà không thể thực hiện được trong bối cảnh không nghiêm ngặt.
  • Theo mặc định, các tập lệnh được tải bằng type=module được coi là đã trì hoãn. 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 biệt. Đ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 vào các mô-đun khác và mỗi mô-đun được tải sẽ tạo ra một yêu cầu mạng và tác vụ riêng để đánh giá mô-đun đó.

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

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

  1. Các mô-đun được trì hoãn tải sau sẽ giảm tình trạng 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 đó. Điều này giúp giải phóng luồng chính để luồng này có thể phản hồi nhanh hơn các hoạt động tương tác của người dùng.
  2. Khi thực hiện lệnh gọi import() động, mỗi lệnh gọi sẽ tách biệt hiệu quả việc biên dịch và đánh giá từng mô-đun thành nhiệm vụ riêng. Tất nhiên, một import() động tải một mô-đun rất lớn sẽ bắt đầu một tác vụ đánh giá tập lệnh khá lớn và điều đó có thể ảnh hưởng đến khả năng luồng chính phản hồi hoạt động đầu vào của người dùng 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 thu được sẽ giống với số lượng mô-đun được nhập động.

Tập lệnh được tải trong một worker web

Trình chạy web là một trường hợp sử dụng JavaScript đặc biệt. Worker web được đăng ký trên luồng chính và mã trong worker sẽ chạy trên luồng riêng. Điều này rất có lợi vì mặc dù mã đăng ký worker web chạy trên luồng chính, nhưng mã trong worker web lại không chạy. Điều này làm giảm tình trạng tắc 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 chạy web có thể tải các tập lệnh bên ngoài để sử dụng trong ngữ cảnh của trình chạy, thông qua importScripts hoặc câu lệnh import tĩnh trong các trình duyệt hỗ trợ trình chạy mô-đun. Kết quả là mọi tập lệnh do worker web yêu cầu đều được đánh giá ngoài luồng chính.

Lựa chọn đánh đổi và điều cần cân nhắc

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

Hiệu suất nén

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

Trình đóng 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 vào:

  • Đối với webpack, trình bổ trợ SplitChunksPlugin có thể giúp ích. Tham khảo tài liệu về SplitChunksPlugin để biết các tuỳ chọn bạn có thể đặt nhằm giúp quản lý kích thước thành phần.
  • Đối với các trình tạo 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 lệnh gọi import() động trong mã. Các trình tạo gói này (cũng như webpack) sẽ tự động tách thành phần được nhập động vào 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 vai trò quan trọng trong tốc độ tải trang khi người dùng truy cập lại. Khi vận chuyển các gói tập lệnh lớn, nguyên khối, bạn sẽ gặp bất lợi khi lưu vào bộ nhớ đệm của trình duyệt. Lý do là 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 gửi bản sửa lỗi), toàn bộ gói sẽ không hợp lệ và bạn phải tải xuống lại.

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

Mô-đun lồng nhau và hiệu suất tải

Nếu đang vận chuyển các 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, bạn cần lưu ý đến mức độ ảnh hưởng của việc lồng ghép mô-đun đến thời gian khởi động. Lồng ghép mô-đun là khi một mô-đun ES nhập tĩnh một mô-đun ES khác 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 của bạn không được đóng gói cùng nhau, thì mã trước đó sẽ tạo ra một chuỗi yêu cầu mạng: khi a.js được yêu cầu từ một 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 điều này là sử dụng trình tạo gói – nhưng hãy nhớ định cấu hình trình tạo gói để chia nhỏ các tập lệnh nhằm phân bổ công việc đánh giá tập lệnh.

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

Kết luận

Không nghi ngờ gì, việc tối ưu hoá việc đánh giá tập lệnh trong trình duyệt là một nhiệm vụ khó khăn. Phương pháp này phụ thuộc vào các yêu cầu và quy tắc ràng buộc của trang web. Tuy nhiên, bằng cách phân tách tập lệnh, bạn đang phân bổ công việc đánh giá tập lệnh cho nhiều tác vụ nhỏ hơn, nhờ đó giúp luồng chính có thể xử lý các lượt 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 rất lớn vì các tập lệnh này sẽ kích hoạt 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. Hãy chia các tập lệnh của bạn trên nhiều phần tử <script> hơn để chia nhỏ công việc này.
  • Việc sử dụng thuộc tính type=module để tải các mô-đun ES gốc trong trình duyệt sẽ bắt đầu các tác vụ riêng lẻ để đánh giá cho từng tập lệnh mô-đun riêng biệt.
  • 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 hoạt động trong trình tạo gói, vì trình tạo gói sẽ coi mỗi mô-đun được nhập động là một "điểm phân tách", dẫn đến việc tạo một tập lệnh riêng 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 suất 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 liên quan đến công việc đánh giá tập lệnh tốn kém hơn trong ít tác vụ hơn, dẫn đến việc vô hiệu hoá bộ nhớ đệm của trình duyệt, dẫn đến hiệu suất lưu vào bộ nhớ đệm tổng thể thấp hơn.
  • Nếu sử dụng các mô-đun ES gốc mà không cần gói, hãy sử dụng gợi ý 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 gửi càng ít JavaScript càng tốt.

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