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

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

Tuy nhiên, còn các tác vụ dài xuất phát từ việc tải chính các tập lệnh thì sao? Các tác vụ này có thể gây trở ngại cho lượt tương tác của người dùng và ảnh hưởng đến chỉ số INP của trang trong quá trình 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 kích hoạt bằng quá trình đá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, nhờ đó luồng chính 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.

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

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

Hoạt động đánh giá tập lệnh được trực quan 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. Thao tác này gây ra một tác vụ cần nhiều thời gian trong quá trình khởi động, làm ảnh hưởng đến khả năng phản hồi các hoạt động tương tác của người dùng trên luồng chính.
Công việc đánh giá tập lệnh như trong trình phân tích hiệu suất trong Chrome DevTools. Trong trường hợp này, công việc này đủ để gây ra một tác vụ cần nhiều thời gian chặn luồng chính thực hiện các công việc khác, 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 của việc thực thi JavaScript trong 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, 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ây ra 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 đó kết xuất lần đầu. Tuy nhiên, chỉ vì một trang đã kết xuất không có nghĩa là trang đó đã tải xong. Các lượt tương tác diễn ra trong quá trình 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 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 cho 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 kích hoạt phụ thuộc vào việc tập lệnh bạn đang tải đượ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. Vì trình duyệt có xu hướng xử lý mọi thứ theo cách khác nhau, nên chúng ta sẽ đề cập đến cách các công cụ trình duyệt chính xử lý việc đánh giá tập lệnh khi các hành vi đánh giá tập lệnh trên các công cụ này 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 đ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 <script> phần tử sẽ kích hoạt một tác vụ để đánh giá tập lệnh được yêu cầu, nhờ đó tập lệnh có thể được phân tích cú pháp, biên dịch và thực thi. Đây là trường hợp đối với 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 một trình đóng gói để quản lý tập lệnh sản xuất 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 thuộc trường hợp này, thì bạn có thể dự kiến 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 không tốt 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 khối JavaScript lớn và tải nhiều tập lệnh riêng lẻ, nhỏ hơn bằng các phần tử <script> bổ sung.

Mặc dù bạn luôn phải cố gắng tải càng ít JavaScript càng tốt trong quá trình tải trang, nhưng việc chia nhỏ 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 không chặn luồng chính hoặc ít nhất là ít hơn so với ban đầu.

Nhiều tác vụ liên quan đến việc đánh giá tập lệnh như được hình dung 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ì 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 có nhiều phần tử <script> 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ì gói này có nhiều khả năng chặn luồng chính hơn.

Bạn có thể coi việc chia nhỏ các tác vụ để đánh giá tập lệnh là tương tự như việc tạo khoảng trống 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ế tạo khoảng trống sẽ chia nhỏ JavaScript 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 hơn.

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 một cách tự nhiên 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 quá trình sản xuất, đặc biệt là khi được 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 các loại tác vụ khác nhau so với những 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, bao gồm cả hoạt động được gắn nhãn là Biên dịch mô-đun.

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

Sau khi các mô-đun đã 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á kịp thời một mô-đun như được 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.

Ít nhất là trong Chrome và các trình duyệt liên quan, hiệu ứng ở đây là các bước biên dịch sẽ 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 kết quả 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 càng ít JavaScript càng tốt, nhưng việc sử dụng các mô-đun ES (bất kể trình duyệt nào) sẽ mang lại những lợi ích sau:

  • Tất cả mã mô-đun đều tự động chạy ở chế độ nghiêm ngặt. Điều này cho phép các công cụ JavaScript tối ưu hoá tiềm năng mà nếu không thì không thể thực hiện trong bối cảnh không nghiêm ngặt.
  • Các tập lệnh được tải bằng type=module được coi như thể chúng được 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 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 duy nhất chỉ bao gồm các câu lệnh static import cho các mô-đun khác và mọi mô-đun được tải sẽ phát sinh một yêu cầu mạng và tác vụ riêng biệt để đánh giá mô-đun đó.

Tập lệnh được tải bằng import() động

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

import() động có 2 ưu điểm khi cải thiện chỉ số INP:

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

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

Web worker là một trường hợp sử dụng JavaScript đặc biệt. Web worker được đăng ký trên luồng chính và mã trong worker sau đó chạy trên luồng riêng. Điều này rất có lợi theo nghĩa là mặc dù mã đăng ký web worker chạy trên luồng chính, nhưng mã trong web worker 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 lượt 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, web worker bản thân có thể tải các tập lệnh bên ngoài để sử dụng trong ngữ cảnh worker, thông qua importScripts hoặc cá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 được web worker 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ù việc chia nhỏ tập lệnh thành các tệp riêng biệt, nhỏ hơn giúp giới hạn các tác vụ dài thay vì 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 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, hiệu quả nén sẽ giảm đi. 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 cân bằng để đảm bảo rằng bạn đang chia nhỏ tập lệnh thành đủ các khối 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ác 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:

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ải trang nhanh như thế nào 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 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 gửi bản sửa lỗi), toàn bộ gói sẽ trở nên không hợp lệ và phải được 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 thành các tác vụ nhỏ hơn mà còn 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 giúp tải trang nhanh hơn.

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

Nếu bạn đang gửi các mô-đun ES trong quá trình sản xuất và tải chúng bằng thuộc tính type=module, thì bạn cần biết cách việc lồng mô-đun có thể ảnh hưởng đến thời gian khởi động. Việc lồng mô-đun đề cập đến trường hợp 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 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 đi cho b.js, sau đó sẽ 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 đóng gói, nhưng hãy đảm bảo rằng bạn đang định cấu hình trình đóng gói để chia nhỏ tập lệnh nhằm phân tán 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. 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á quá trình đánh giá tập lệnh trong trình duyệt chắc chắn là một việc khó khăn. Phương pháp này phụ thuộc vào các yêu cầu và ràng buộc của trang web. Tuy nhiên, bằng cách chia nhỏ tập lệnh, bạn đang phân tán công việc đánh giá tập lệnh trên nhiều tác vụ nhỏ hơn và do đó, giúp luồng chính có thể xử lý các lượt tương tác của người dùng một cách 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> mà 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 và chặn luồng chính. Hãy phân tán tập lệnh 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ẽ kích hoạt 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 các lệnh gọi import() động. Điều này cũng hoạt độ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 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 lựa chọn đá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 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 và dẫn đến việc vô hiệu hoá bộ nhớ đệm của trình duyệt, khiến hiệu quả nén tổng thể thấp hơn.
  • Nếu sử dụng các mô-đun ES một cách tự nhiên mà không đóng gói, hãy sử dụng gợi ý tài nguyên modulepreload để tối ưu hoá quá trình tải các mô-đun này 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 việc cần 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 lượt 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.