Đá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ể 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 tính năng đánh giá tập lệnh và những việc bạn có thể làm để tập lệnh không gây ra các thao tác dài trong quá trình tải trang.

Liên quan đến việc tối ưu hoá Lượt tương tác với nội dung hiển thị tiếp theo (INP), hầu hết lời khuyên mà bạn gặp phải là tự tối ưu hoá các lượt tương tác. Ví dụ: trong hướng dẫn tối ưu hoá tác vụ dài, chúng tôi sẽ thảo luận về các kỹ thuật như tạo kết quả bằng setTimeout và các kỹ thuật khác. Những kỹ thuật này có lợi vì chúng cho phép luồng chính có khoảng trống để thở bằng cách tránh các nhiệm vụ dài. Việc này có thể tạo ra nhiều cơ hội tương tác và hoạt động khác diễn ra sớm hơn, thay vì phải đợi một nhiệm vụ dài.

Tuy nhiên, những tác vụ dài bắt nguồn từ việc tải tập lệnh thì sao? Các 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 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ụ được bắt đầu bằng hoạt động đánh giá tập lệnh, đồ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 sao cho luồng chính của bạn có thể phản hồi nhanh hơn với hoạt động đầu vào của người dùng trong khi trang đang tải.

Hoạt động đánh giá tập lệnh là gì?

Nếu đã lập hồ sơ một ứng dụng gửi nhiều JavaScript, thì bạn có thể đã thấy các tác vụ dài trong đó thủ phạm được gắn nhãn Đánh giá tập lệnh.

Việc đánh giá tập lệnh hoạt động như minh hoạ trong trình phân tích hiệu suất của Chrome Công cụ cho nhà phát triển. Công việc 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 các hoạt động tương tác của người dùng.
Hoạt động đánh giá tập lệnh hoạt động như 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 đó đủ để gây ra một tác vụ dài khiến luồng chính không thể đảm nhận các tác vụ khác nữa, bao gồm cả các tác vụ thúc đẩy lượt tương tác của người dùng.

Đánh giá tập lệnh là một phần cần thiết trong quá trình thực thi JavaScript trên trình duyệt, vì JavaScript được biên dịch ngay 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, thì 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, 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 trang ngay sau khi trang đó hiển thị lần đầu. Tuy nhiên, việc một trang đã được hiển thị không có nghĩa là trang đó đã tải xong. Các hoạt động tương tác diễn ra trong khi tải có thể bị trễ do trang đang bận đánh giá các tập lệnh. Mặc dù không có gì đả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ề hành động đó có thể chưa được 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 hoạt động tương tác hoàn toàn không phụ thuộc vào JavaScript.

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

Cách bắt đầu các nhiệm 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ó được tải bằng 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. Do các trình duyệt có xu hướng xử lý mọi thứ 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 xem xét khi hành vi đánh giá tập lệnh trên chúng thay đổi.

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

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

Mặc dù bạn phải luôn cố 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 của bạn đả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 và không chặn luồng chính – hoặc ít nhất là ít hơn so với những gì bạn đã bắt đầu.

Nhiều nhiệm vụ liên quan đến việc đánh giá tập lệnh như minh hoạ trong trình phân tích hiệu suất của Công cụ cho nhà phát triển của Chrome. Vì có nhiều tập lệnh nhỏ được tải thay vì có í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 nhiệm vụ được tạo ra để đánh giá tập lệnh nhờ có 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ì 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 cũng tương tự như việc tạo ra lợi nhuận 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ế lợi nhuận chia nhỏ JavaScript mà bạn tải thành nhiều tập lệnh nhỏ hơn, thay vì số lượng tập lệnh lớn hơn có nhiều khả năng chặn chuỗi chính.

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 ngay 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 cho phiên bản chính thức, đặc biệt là khi 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ụ khác với những loại tác vụ 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 có liên quan đến hoạt động có nhãn là Mô-đun biên dịch.

Biên dịch mô-đun hoạt động trong nhiều tác vụ như minh 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 Mô-đun biên dịch để biên dịch nội dung của chúng trước khi đánh giá.

Sau khi các mô-đun này đã 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 là Đánh giá mô-đun.

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

Ảnh hưở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 mô-đun ES. Đây là một thành công rõ ràng trong việc quản lý các tác vụ dài; tuy nhiên, kết quả đánh giá mô-đun vẫn có nghĩa là bạn đang phải chịu một số chi phí không 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 là gì—mang lại các lợi ích sau:

  • Tất cả mã mô-đun đều được tự động chạy ở chế độ nghiêm ngặt, cho phép các công cụ JavaScript có thể thực hiện tối ưu hoá trong bối cảnh không nghiêm ngặt.
  • Các tập lệnh được tải bằng cách sử dụng type=module được xử lý như thể các tập lệ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. Tức 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 tĩnh import cho các mô-đun khác, và mỗi mô-đun đã tải sẽ phát sinh 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ác câu lệnh import tĩnh bắt buộc phải ở đầ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 đoạ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 lợi thế trong việc cải thiện INP:

  1. Các mô-đun bị trì hoãn tải sau này 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 đó. Việc này giải phóng luồng chính để luồng chính phản hồi nhanh hơn với các tương tác của người dùng.
  2. Khi thực hiện lệnh gọi import() linh động, mỗi lệnh gọi sẽ tách riêng hoạt động biên dịch và đánh giá của từng mô-đun thành từng tác 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 nhiệm vụ đánh giá tập lệnh khá lớn và 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() linh động. Do đó, bạn vẫn cần phải tải càng ít JavaScript càng tốt.

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

Các tập lệnh được tải trong một trình chạy web

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

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

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

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

Hiệu suất nén

Nén là một yếu tố khi chia nhỏ tập lệnh. Khi các 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ẽ được hưởng lợi nhiều hơn từ tính năng nén. Mặc dù việc tăng hiệu quả nén giúp giữ thời gian tải cho tập lệnh ở mức thấp nhất có thể, nhưng đó là một hoạt động cân bằng để đảm bảo rằng bạn chia nhỏ 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 gói dữ liệu 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:

  • Trong trường hợp liên quan đến webpack, trình bổ trợ SplitChunksPlugin của gói 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 các trình gói khác như Tổng hợpbản dựng, 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ủa mình. Các trình gói này (cũng như webpack) sẽ tự động chia nhỏ thành phần được nhập tự độ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 việc tốc độ tải trang trong các lượt 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 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 bản sửa lỗi vận chuyển), toàn bộ gói sẽ không còn 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 của mì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 làm tăng khả năng khách truy cập quay lại 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 sẽ chuyển thành tải trang nhanh hơn về tổng thể.

Các 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, thì bạn cần lưu ý về tác động của việc lồng mô-đun đến thời gian khởi động. Lồng mô-đun là trường hợp một mô-đun ES nhập tĩnh một mô-đun ES khác mà 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 lại với nhau, 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ừ 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 gói – nhưng hãy đảm bảo bạn đang định cấu hình trình tạo gói của mình để chia nhỏ các tập lệnh nhằm mở rộng công việc đánh giá tập lệnh.

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

Kết luận

Tối ưu hoá việc đánh giá các 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. Tuy nhiên, bằng cách chia nhỏ các tập lệnh, bạn đang mở rộng công việc đánh giá tập lệnh lên nhiều tác vụ nhỏ hơn, từ đó mang lại cho luồng chính khả năng xử lý 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 nhiệm vụ đánh giá tập lệnh lớn:

  • Khi tải các tập lệnh bằng phần tử <script> mà không có thuộc tính type=module, hãy tránh tải các tập lệnh có kích thước quá lớn, vì các tập lệnh này sẽ bắt đầu các nhiệm vụ đánh giá tập lệnh tốn nhiều tài nguyên chặn luồng chính. Mở rộng 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 một cách tự nhiên trong trình duyệt sẽ bắt đầu từng tác vụ riêng lẻ để đánh giá cho từng tập lệnh mô-đun riêng biệt.
  • Hãy 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 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 việc một tập lệnh riêng sẽ được tạo cho mỗi mô-đun được nhập động.
  • Hãy nhớ cân nhắc đánh đổi, chẳng hạn như hiệu quả nén và 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 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 việc 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 nói chung thấp hơn.
  • Nếu bạn sử dụng các mô-đun ES một cách tự nhiên mà không 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 ít JavaScript nhất có thể.

Đó chắc chắn là hoạt động cân bằng. Tuy nhiên, 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à phù hợp hơn với 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 được điểm số cao hơn dựa trên chỉ số INP, từ đó mang lại trải nghiệm người dùng tốt hơn.

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