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

Addy Osmani
Addy Osmani

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

Mạng

Khi nghĩ về chi phí của JavaScript, hầu hết các nhà phát triển đều nghĩ về chi phí tải xuống và thực thi. Việc gửi thêm byte JavaScript qua đường dây sẽ mất nhiều thời gian hơn để 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 các tài nguyên như JavaScript, các tài nguyên đó phải được phân tích cú pháp và biên dịch trước khi thực thi.

Đây có thể là một vấn đề, do 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ể sử dụng Wi-Fi của quán cà phê nhưng được kết nối với điểm phát sóng di động có tốc độ 2G.

Bạn có thể giảm chi phí chuyển 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 những gì quan trọng và không quan trọng. Các gói mô-đun như gói web hỗ trợ phân tách mã.
    • Tải từng phần một đoạn mã không quan trọng.
  • Giảm thiểu
  • 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.
    • Hãy cân nhắc sử dụng Brotli ~q11. Brotli tốt hơn gzip về tỷ lệ nén. Tính năng này đã giúp CertSimple tiết kiệm được 17% kích thước của các byte JS được nén và LinkedIn tiết kiệm được 4% thời gian tải.
  • Xoá đoạn mã không dùng đến.
  • Lưu mã vào bộ nhớ đệm để giảm thiểu các chuyến đi 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 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 các tập lệnh (max-age) và mã thông báo xác thực nguồn cung (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 có khả năng phục hồi và cho phép bạn truy cập nhanh hơn vào các tính năng như bộ nhớ đệm mã của V8.
    • Sử dụng chức năng lưu vào bộ nhớ đệm trong thời gian dài để tránh phải tìm nạp lại các tài nguyên không thay đổi. Nếu sử dụng Webpack, hãy xem phần 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í nặng 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 Công cụ phát triển Chrome, phân tích cú pháp và biên dịch là một phần của thời gian "Tập lệnh" màu vàng trong bảng điều khiển Hiệu suất.

ALT_TEXT_HERE

Các tab Từ dưới lên và Cây cuộc gọi hiển thị cho bạn thời gian Phân tích cú pháp/biên dịch chính xác:

ALT_TEXT_HERE
Bảng điều khiển Hiệu suất Công cụ cho nhà phát triển Chrome > Từ dưới lên. Khi bật tính năng Thống kê cuộc 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 dành một thời gian dài để phân tích cú pháp/biên dịch mã có thể làm chậm trễ đá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 JavaScript trước khi trang web của bạn tương tác được.

Theo byte cho byte, JavaScript cho trình duyệt xử lý tốn kém hơn so với hình ảnh có kích thước tương đương hoặc phông chữ web — 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 (chúng vẫn phải được giải mã!) nhưng trên phần cứng di động thông thường, JS có nhiều khả năng tác động tiêu cực đến khả năng tương tác của trang hơn.

ALT_TEXT_HERE
JavaScript và 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 hoạt độ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 tôi nói về việc phân tích cú pháp và biên dịch chậm; ngữ cảnh rất quan trọng — chúng tôi đang nói về điện thoại di động trung bình ở đây. Người dùng trung bình có thể sở hữu đ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ế về bộ nhớ.

Khả năng của mạng và khả năng của thiết bị không phải lúc nào cũng phù hợp. Người dùng có kết nối Fiber 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 khi đảo ngược... một kết nối mạng rất tệ nhưng CPU lại cực nhanh. – Kristofer Baxter, LinkedIn

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

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

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

Trên iPhone 8 cao cấp, bạn 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 điện thoại thông thường (Moto G4). Điều này có thể ảnh hưởng đáng kể đến tốc độ mà 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 nêu bật 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ỉ trên điện thoại có thể nằm trong túi của bạn. Tuy nhiên, ngữ cảnh vẫn quan trọng: tối ưu hoá cho điều kiện thiết bị và mạng mà người dùng của bạn gặp phải.

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

Chúng tôi có thực sự đang gửi xuống quá nhiều JavaScript không? Ồ, có thể là :)

Khi sử dụng Kho lưu trữ HTTP (khoảng 500 nghìn 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 tôi có thể thấy rằng 50% trang web mất hơn 14 giây để tương tác. Các trang web này dành tối đa 4 giây để 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. Không có gì đáng ngạc nhiên khi người dùng có thể phải đợi một lúc trước khi cảm thấy các trang đã sẵn sàng sử dụng. Chắc chắn là chúng tôi có thể làm tốt hơn ở đây.

Việc xoá JavaScript không quan trọng khỏi các trang của bạn có thể làm giảm thời gian truyền, việc phân tích cú pháp và biên dịch tốn nhiều CPU cũng như giảm chi phí bộ nhớ tiềm ẩn. Tính năng 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

Việc này không chỉ giúp phân tích cú pháp và biên dịch mà có thể gây tốn kém. Thực thi JavaScript (chạy mã sau khi được 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 dài cũng có thể cho biết thời điểm người dùng có thể 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ì hoãn bởi toàn bộ khoảng 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 được hưởng lợi từ việc phân thành các phần nhỏ để tránh bị khoá luồng chính. 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ể tác động đến hiệu suất của trang theo những cách khác:

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

Các mô hình để giảm chi phí phân phối JavaScript

Khi bạn đang 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 ở mức 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 (Đạo luật bảo vệ quyền riêng tư của trẻ em)

PRPL (Đẩy, Kết xuất, Trước bộ nhớ đệm, Tải từng phần) là một mẫu tối ưu hoá khả năng tương tác thông qua tính năng phân tách mã và lưu vào bộ nhớ đệm linh hoạt:

ALT_TEXT_HERE

Hãy hình dung tác động của quy trình này.

Chúng tôi phân tích thời gian tải của các trang web dành cho thiết bị di động phổ biến và Ứng dụng web tiến bộ bằng cách sử dụng Số liệu thống kê cuộc 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 quan trọng mà nhiều trang web trong số này dành thời gian:

ALT_TEXT_HERE

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

Tự thân khởi nghiệp tăng tiến

Nhiều trang web tối ưu hoá khả năng hiển thị nội dung với cái giá là tương tác cao. Để có được lần vẽ đầu tiên nhanh 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" nó để đí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 — việc này cũng có chi phí riêng. Bạn 1) thường gửi xuống một phản hồi HTML lớn hơn có thể đẩy tính tương tác của chúng tôi, 2) có thể khiến người dùng rơi vào một thung lũng kỳ lạ nơi một nửa trải nghiệm không thể thực sự tương tác được cho đến khi JavaScript hoàn tất xử lý.

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

ALT_TEXT_HERE
Tiến trình Bootstrapping của Paul Lewis

Tải mã tương ứng với nội dung mà bạn nhìn thấy. PRPL và Kiểu tự khởi động tiến bộ là các mẫu có thể giúp thực hiện việc này.

Kết luận

Kích thước truyền 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 là rất quan trọng đối với các thiết bị ràng buộc CPU. Duy trì những vấn đề thấp này.

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

ALT_TEXT_HERE
Sẽ thật hữu ích khi cân nhắc xem các quyết định về mặt cấu trúc mà chúng ta đưa ra có thể giải quyết được logic của ứng dụng như thế nào.

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 hết sức để phát triển trên phần cứng đại diện, duy trì thời gian phân tích/biên dịch JavaScript ở mức thấp 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í cho JavaScript.

Tìm hiểu thêm