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ụ 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 đế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).
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), 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 và 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.
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).
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).
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 ngữ 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ínhasync
trên các tập lệnh được tải bằngtype=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 đó.
Các 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:
- 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.
- 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ộtimport()
động tải một mô-đun rất lớn sẽ kích hoạt một tác vụ đánh giá tập lệnh khá lớn và điều này 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ọiimport()
độ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à sau đó 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 thì không. Điều này giúp 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 worker web có thể tải tập lệnh bên ngoài để sử dụng trong ngữ cảnh worker, thông qua importScripts
hoặc câu lệnh import
tĩnh trong các trình duyệt hỗ trợ worker 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 hạn chế 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ư Rollup và esbuild, 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 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 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 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 đóng 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ínhtype=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à đáp ứng tốt hơn 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 trên chỉ số INP, từ đó mang lại trải nghiệm tốt hơn cho người dùng.