Tối ưu hoá Khởi động JavaScript

Khi xây dựng các trang web phụ thuộc nhiều hơn vào JavaScript, đôi khi chúng ta phải trả tiền cho những gì chúng ta gửi xuống theo những cách mà không phải lúc nào chúng ta cũng dễ dàng nhận thấy. Trong bài viết này, chúng tôi sẽ trình bày lý do vì sao một chút kỷ luật có thể giúp ích nếu bạn muốn trang web của mình tải và tương tác nhanh chóng trên thiết bị di động. Việc phân phối ít JavaScript hơn có thể giúp giảm thời gian truyền tải qua mạng, giảm thời gian giải nén mã và giảm thời gian phân tích cú pháp cũng như biên dịch JavaScript này.

Mạng

Khi hầu hết các nhà phát triển nghĩ về chi phí của JavaScript, họ nghĩ về chi phí này theo chi phí tải xuống và chi phí thực thi. Việc gửi nhiều byte JavaScript hơn qua mạng sẽ mất nhiều thời gian hơn khi kết nối của người dùng chậm hơn.

Khi trình duyệt yêu cầu một tài nguyên, tài nguyên đó cần được tìm nạp rồi giải nén. Trong trường hợp tài nguyên như JavaScript, bạn phải phân tích cú pháp và biên dịch các tài nguyên đó trước khi thực thi.

Đây có thể là vấn đề vì loại kết nối mạng hiệu quả mà người dùng có thể không thực sự là 3G, 4G hoặc Wi-Fi. Bạn có thể đang sử dụng Wi-Fi của quán cà phê nhưng lại kết nối với một điểm phát sóng di động có tốc độ 2G.

Bạn có thể giảm chi phí truyền tải mạng của JavaScript thông qua:

  • Chỉ gửi mã mà người dùng cần.
    • Sử dụng tính năng phân tách mã để chia JavaScript thành phần quan trọng và không quan trọng. Trình đóng gói mô-đun như webpack hỗ trợ phân tách mã.
    • Tải từng phần mã không quan trọng.
  • Rút gọn
  • Nén
    • Tối thiểu, hãy sử dụng gzip để nén các tài nguyên dựa trên văn bản.
    • Cân nhắc sử dụng Brotli ~q11. Brotli vượt trội hơn gzip về tỷ lệ nén. Nhờ đó, CertSimple đã tiết kiệm được 17% về kích thước của các byte JS được nén và LinkedIn tiết kiệm được 4% về thời gian tải.
  • Xoá mã không dùng đến.
  • Lưu mã vào bộ nhớ đệm để giảm thiểu số lần truy cập mạng.
    • Sử dụng tính năng lưu vào bộ nhớ đệm HTTP để đảm bảo trình duyệt lưu các phản hồi vào bộ nhớ đệm một cách hiệu quả. Xác định thời gian hoạt động tối ưu cho tập lệnh (max-age) và cung cấp mã xác thực (ETag) để tránh chuyển các byte không thay đổi.
    • Việc lưu vào bộ nhớ đệm của Trình chạy dịch vụ có thể giúp mạng ứng dụng của bạn linh hoạt và cho phép bạn truy cập nhanh vào các tính năng như bộ nhớ đệm mã của V8.
    • Sử dụng tính năng lưu vào bộ nhớ đệm dài hạn để tránh phải tìm nạp lại các tài nguyên chưa thay đổi. Nếu bạn đang sử dụng Webpack, hãy xem phần hàm băm tên tệp.

Phân tích cú pháp/Biên dịch

Sau khi tải xuống, một trong những chi phí lớn nhất của JavaScript là thời gian để công cụ JS phân tích cú pháp/biên dịch mã này. Trong Chrome DevTools, quá trình phân tích cú pháp và biên dịch là một phần của thời gian "Viết tập lệnh" màu vàng trong bảng điều khiển Hiệu suất.

ALT_TEXT_HERE

Thẻ Bottom-Up (Dưới lên) và Call Tree (Cây lệnh gọi) cho bạn biết thời gian chính xác để Phân tích cú pháp/biên dịch:

ALT_TEXT_HERE
Bảng điều khiển Hiệu suất của Chrome DevTools > Từ dưới lên. Khi bật Số liệu thống kê về lệnh gọi trong thời gian chạy của V8, chúng ta có thể thấy thời gian dành cho các giai đoạn như Phân tích cú pháp và Biên dịch

Nhưng tại sao điều này lại quan trọng?

ALT_TEXT_HERE

Việc mất nhiều thời gian để phân tích cú pháp/biên dịch mã có thể làm chậm đáng kể thời gian người dùng có thể tương tác với trang web của bạn. Bạn gửi càng nhiều JavaScript thì càng mất nhiều thời gian để phân tích cú pháp và biên dịch trước khi trang web có thể tương tác.

Theo từng byte, trình duyệt sẽ tốn nhiều tài nguyên hơn để xử lý JavaScript so với hình ảnh hoặc Phông chữ web có kích thước tương đương — Tom Dale

So với JavaScript, có rất nhiều chi phí liên quan đến việc xử lý hình ảnh có kích thước tương đương (vẫn phải giải mã!) nhưng trên phần cứng di động trung bình, JS có nhiều khả năng ảnh hưởng tiêu cực đến khả năng tương tác của trang.

ALT_TEXT_HERE
JavaScript và các byte hình ảnh có chi phí rất khác nhau. Hình ảnh thường không chặn luồng chính hoặc ngăn giao diện tương tác trong khi được giải mã và tạo điểm ảnh. Tuy nhiên, JS có thể trì hoãn khả năng tương tác do chi phí phân tích cú pháp, biên dịch và thực thi.

Khi chúng ta nói về việc phân tích cú pháp và biên dịch bị chậm; ngữ cảnh là rất quan trọng — chúng ta đang nói về điện thoại di động trung bình. Người dùng thông thường có thể dùng điện thoại có CPU và GPU chậm, không có bộ nhớ đệm L2/L3 và thậm chí có thể bị hạn chế bộ nhớ.

Không phải lúc nào chức năng mạng và chức năng thiết bị cũng khớp nhau. Người dùng có kết nối cáp quang tuyệt vời không nhất thiết phải có CPU tốt nhất để phân tích cú pháp và đánh giá JavaScript được gửi đến thiết bị của họ. Điều này cũng đúng theo hướng ngược lại… kết nối mạng rất kém nhưng CPU lại cực nhanh. – Kristofer Baxter, LinkedIn

Dưới đây, chúng ta có thể thấy chi phí phân tích cú pháp khoảng 1 MB JavaScript đã giải nén (đơn giản) trên phần cứng cấp thấp và cấp cao. Thời gian phân tích cú pháp/biên dịch mã giữa điện thoại nhanh nhất trên thị trường và điện thoại trung bình có sự khác biệt từ 2 đến 5 lần.

ALT_TEXT_HERE
Biểu đồ này làm nổi bật thời gian phân tích cú pháp cho một gói JavaScript 1 MB (~250 KB được nén bằng gzip) trên máy tính để bàn và thiết bị di động thuộc các lớp khác nhau. Khi xem xét chi phí phân tích cú pháp, bạn cần xem xét các con số đã giải nén, ví dụ: ~250 KB JS nén bằng gzip giải nén thành ~1 MB mã.

Còn trang web thực tế như CNN.com thì sao?

Trên iPhone 8 cao cấp, chỉ mất khoảng 4 giây để phân tích cú pháp/biên dịch JS của CNN so với khoảng 13 giây đối với một chiếc điện thoại trung bình (Moto G4). Điều này có thể ảnh hưởng đáng kể đến tốc độ người dùng có thể tương tác đầy đủ với trang web này.

ALT_TEXT_HERE
Ở trên, chúng ta thấy thời gian phân tích cú pháp so sánh hiệu suất của chip A11 Bionic của Apple với Snapdragon 617 trong phần cứng Android trung bình hơn.

Điều này nhấn mạnh tầm quan trọng của việc kiểm thử trên phần cứng trung bình (như Moto G4) thay vì chỉ kiểm thử trên điện thoại có thể nằm trong túi của bạn. Tuy nhiên, bối cảnh cũng quan trọng: hãy tối ưu hoá cho điều kiện thiết bị và mạng mà người dùng của bạn có.

ALT_TEXT_HERE
Google Analytics có thể cung cấp thông tin chi tiết về các loại thiết bị di động mà người dùng thực đang truy cập vào trang web của bạn. Điều này có thể giúp bạn hiểu được các giới hạn thực sự của CPU/GPU mà ứng dụng đang hoạt động.

Chúng ta có thực sự gửi quá nhiều JavaScript không? Có thể là vậy :)

Khi sử dụng Kho lưu trữ HTTP (khoảng 500.000 trang web hàng đầu) để phân tích trạng thái của JavaScript trên thiết bị di động, chúng ta có thể thấy rằng 50% trang web mất hơn 14 giây để có thể tương tác. Những trang web này mất tới 4 giây chỉ để phân tích cú pháp và biên dịch JS.

ALT_TEXT_HERE

Hãy tính đến thời gian cần thiết để tìm nạp và xử lý JS cũng như các tài nguyên khác. Có thể bạn sẽ không ngạc nhiên khi người dùng phải đợi một lúc trước khi cảm thấy trang đã sẵn sàng để sử dụng. Chúng ta chắc chắn có thể làm tốt hơn ở đây.

Việc xoá JavaScript không quan trọng khỏi các trang có thể làm giảm thời gian truyền, quá trình phân tích cú pháp và biên dịch tốn nhiều CPU cũng như mức hao tổn bộ nhớ tiềm ẩn. Điều này cũng giúp các trang của bạn tương tác nhanh hơn.

Thời gian thực thi

Không chỉ việc phân tích cú pháp và biên dịch mới có thể gây tốn kém. Thực thi JavaScript (chạy mã sau khi phân tích cú pháp/biên dịch) là một trong những thao tác phải diễn ra trên luồng chính. Thời gian thực thi lâu cũng có thể làm chậm thời gian người dùng tương tác với trang web của bạn.

ALT_TEXT_HERE

Nếu tập lệnh thực thi trong hơn 50 mili giây, thì thời gian tương tác sẽ bị trễ toàn bộ thời gian cần thiết để tải xuống, biên dịch và thực thi JS — Alex Russell

Để giải quyết vấn đề này, JavaScript sẽ được chia thành các phần nhỏ để tránh khóa luồng chính. Hãy tìm hiểu xem bạn có thể giảm lượng công việc đang thực hiện trong quá trình thực thi hay không.

Các chi phí khác

JavaScript có thể ảnh hưởng đến hiệu suất trang theo những cách khác:

  • Bộ nhớ. Các trang có thể xuất hiện giật hoặc thường xuyên tạm dừng do GC (thu gom rác). Khi trình duyệt thu hồi bộ nhớ, quá trình thực thi JS sẽ bị tạm dừng, vì vậy, trình duyệt thường xuyên thu thập rác có thể tạm dừng quá trình thực thi thường xuyên hơn mức chúng ta mong muốn. Tránh rò rỉ bộ nhớ và các điểm tạm dừng gc thường xuyên để các trang không bị giật.
  • Trong thời gian chạy, JavaScript chạy trong thời gian dài có thể chặn luồng chính khiến các trang không phản hồi. Việc chia công việc thành các phần nhỏ hơn (sử dụng requestAnimationFrame() hoặc requestIdleCallback() để lập lịch biểu) có thể giảm thiểu các vấn đề về khả năng phản hồi, giúp cải thiện Lượt tương tác đến nội dung hiển thị tiếp theo (INP).

Các mẫu để giảm chi phí phân phối JavaScript

Khi bạn cố gắng duy trì thời gian phân tích cú pháp/biên dịch và truyền mạng cho JavaScript chậm, có một số mẫu có thể giúp ích như phân đoạn dựa trên tuyến hoặc PRPL.

PRPL

PRPL (Đẩy, Hiển thị, Lưu vào bộ nhớ đệm trước, Tải lười) là một mẫu tối ưu hoá tính tương tác thông qua việc phân tách mã và lưu vào bộ nhớ đệm một cách mạnh mẽ:

ALT_TEXT_HERE

Hãy cùng hình dung tác động của việc này.

Chúng tôi phân tích thời gian tải của các trang web phổ biến dành cho thiết bị di động và Ứng dụng web tiến bộ bằng cách sử dụng Số liệu thống kê về lệnh gọi trong thời gian chạy của V8. Như chúng ta có thể thấy, thời gian phân tích cú pháp (hiển thị bằng màu cam) là một phần đáng kể trong thời gian mà nhiều trang web này dành ra:

ALT_TEXT_HERE

Wego, một trang web sử dụng PRPL, có thể duy trì thời gian phân tích cú pháp thấp cho các tuyến đường của họ, tương tác rất nhanh. Nhiều trang web khác trong số này đã áp dụng tính năng phân tách mã và ngân sách hiệu suất để cố gắng giảm chi phí JS.

Tự khởi động từng phần

Nhiều trang web tối ưu hoá khả năng hiển thị nội dung mà không chú trọng đến khả năng tương tác. Để có được lượt vẽ đầu tiên nhanh chóng khi bạn có các gói JavaScript lớn, đôi khi nhà phát triển sử dụng tính năng kết xuất phía máy chủ; sau đó "nâng cấp" tính năng này để đính kèm trình xử lý sự kiện khi JavaScript cuối cùng được tìm nạp.

Hãy cẩn thận vì việc này sẽ phát sinh chi phí. Bạn 1) thường gửi một phản hồi HTML lớn hơn có thể thúc đẩy khả năng tương tác của chúng ta, 2) có thể khiến người dùng rơi vào một hố sâu kỳ lạ, trong đó một nửa trải nghiệm không thực sự có thể tương tác cho đến khi JavaScript hoàn tất quá trình xử lý.

Bạn nên sử dụng phương pháp Bootstrap tăng dần. Gửi một trang có chức năng tối thiểu (chỉ bao gồm HTML/JS/CSS cần thiết cho tuyến hiện tại). Khi có thêm tài nguyên, ứng dụng có thể tải lười và mở khoá thêm nhiều tính năng.

ALT_TEXT_HERE
Tự động khởi động tăng tiến của Paul Lewis

Tải mã tương ứng với nội dung hiển thị là mục tiêu hàng đầu. PRPL và tính năng Tự động tải dần là các mẫu có thể giúp bạn thực hiện việc này.

Kết luận

Kích thước truyền tải rất quan trọng đối với các mạng cấp thấp. Thời gian phân tích cú pháp rất quan trọng đối với các thiết bị bị giới hạn bởi CPU. Bạn cần duy trì các chỉ số này ở mức thấp.

Các nhóm đã thành công khi áp dụng ngân sách hiệu suất nghiêm ngặt để giảm thời gian truyền và phân tích cú pháp/biên dịch JavaScript. Xem video "Can You Buy It?" (Bạn có đủ tiền mua không?) của Alex Russell: Ngân sách hiệu suất thực tế trên web" để biết hướng dẫn về ngân sách cho thiết bị di động.

ALT_TEXT_HERE
Bạn nên cân nhắc xem các quyết định về cấu trúc mà chúng ta đưa ra có thể để lại bao nhiêu "khoảng đệm" JS cho logic ứng dụng.

Nếu bạn đang xây dựng một trang web nhắm đến thiết bị di động, hãy cố gắng phát triển trên phần cứng đại diện, giảm thời gian phân tích cú pháp/biên dịch JavaScript và sử dụng Ngân sách hiệu suất để đảm bảo nhóm của bạn có thể theo dõi chi phí JavaScript.

Tìm hiểu thêm