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ả giá cho những gì chúng ta gửi xuống theo những cách không phải lúc nào cũng dễ dàng nhận thấy. Trong bài viết này, chúng ta sẽ tìm hiểu lý do tại 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à có thể 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 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 nghĩ về chi phí của JavaScript, hầu hết nhà phát triển đều nghĩ đến chi phí tải xuống và thực thi. Việc gửi nhiều byte JavaScript 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.

Đây có thể là một 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 dùng Wi-Fi của quán cà phê nhưng lại 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í truyền JavaScript qua mạng bằng cách:
- Chỉ gửi mã mà người dùng cần.
- Sử dụng tính năng tách mã để chia JavaScript thành phần quan trọng và phần không quan trọng. Các trình đóng gói mô-đun như webpack hỗ trợ phân tách mã.
- Tải chậm mã không quan trọng.
- Rút gọn
- Sử dụng UglifyJS để rút gọn mã ES5.
- Sử dụng babel-minify hoặc uglify-es để giảm thiểu ES2015+.
- Nén
- Xoá mã không dùng đến.
- Xác định các cơ hội cho mã có thể bị xoá hoặc tải chậm bằng mức độ sử dụng mã của DevTools.
- Sử dụng babel-preset-env và browserlist để tránh chuyển đổi cú pháp các tính năng đã có trong trình duyệt hiện đại. Các nhà phát triển nâng cao có thể thấy rằng việc phân tích kỹ lưỡng các gói webpack sẽ giúp xác định cơ hội để cắt giảm các phần phụ thuộc không cần thiết.
- Để loại bỏ mã, hãy xem phần loại bỏ mã không dùng đến, các tính năng tối ưu hoá nâng cao của Closure Compiler và các trình bổ trợ cắt bớt thư viện như lodash-babel-plugin hoặc ContextReplacementPlugin của webpack cho các thư viện như Moment.js.
- Lưu mã vào bộ nhớ đệm để giảm thiểu số lần truyền dữ liệu qua 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 tập lệnh (max-age) và cung cấp mã thông báo xác thực (ETag) để tránh chuyển các byte không thay đổi.
- Tính năng 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 hoạt động ổn định 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 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 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ụ của Chrome cho nhà phát triển, 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.

Các thẻ Bottom-Up (Từ dưới lên) và Call Tree (Cây lệnh gọi) cho bạn biết thời gian Phân tích cú pháp/biên dịch chính xác:
Nhưng tại sao điều này lại quan trọng?

Việc dành 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 càng gửi 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ủa bạn có thể tương tác.
Xét theo từng byte, trình duyệt tốn nhiều tài nguyên để xử lý JavaScript hơn 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ó 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ã hình ảnh!) 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.
Khi nói về việc phân tích cú pháp và biên dịch chậm, bối cảnh là rất quan trọng — chúng ta đang nói về điện thoại di động trung bình ở đây. Người dùng thông thường có thể sử 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ị giới hạn bộ nhớ.
Không phải lúc nào khả năng của mạng và khả năng của thiết bị cũng khớp nhau. Một 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 trong trường hợp ngược lại… kết nối mạng kém nhưng CPU 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 ~1 MB JavaScript đã giải nén (đơn giản) trên phần cứng cấp thấp và cao cấp. 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 chênh lệch từ 2 đến 5 lần.
Còn một trang web thực tế, chẳng hạn 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 đ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.
Điều này cho thấy tầm quan trọng của việc thử nghiệm trên phần cứng trung bình (như Moto G4) thay vì chỉ thử nghiệm trên điện thoại mà bạn có thể đang dùng. Tuy nhiên, bối cảnh cũng rất quan trọng: hãy tối ưu hoá cho thiết bị và điều kiện mạng mà người dùng của bạn có.
Có phải chúng ta đang gửi quá nhiều JavaScript không? Có thể là vậy :)
Khi sử dụng HTTP Archive (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 đến 4 giây chỉ để phân tích cú pháp và biên dịch JS.

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ó lẽ không có gì ngạc nhiên khi người dùng có thể phải chờ một lúc trước khi cảm thấy các 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ể giảm thời gian truyền, giảm tải cho CPU khi phân tích cú pháp và biên dịch, đồng thời giảm 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 có thể tương tác nhanh hơn.
Thời gian thực thi
Không chỉ có quá trình phân tích cú pháp và biên dịch mới có thể tốn chi phí. 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 dài cũng có thể làm chậm thời điểm người dùng có thể tương tác với trang web của bạn.

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ằng 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 nên được chia thành các đoạn nhỏ để tránh khoá luồng chính. Hãy tìm hiểu xem bạn có thể giảm bớt khối lượng công việc trong quá trình thực thi hay không.
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ể bị giật hoặc tạm dừng thường xuyên do GC (thu gom rác). Khi trình duyệt lấy lạ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 thực thi thường xuyên hơn mức chúng ta mong muốn. Tránh rò rỉ bộ nhớ và tạm dừng gc thường xuyên để giữ cho 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 nhỏ công việc thành các phần nhỏ hơn (sử dụng
requestAnimationFrame()hoặcrequestIdleCallback()để lập lịch biểu) có thể giảm thiểu các vấn đề về khả năng phản hồi, từ đó 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 đang cố gắng giữ cho thời gian phân tích cú pháp/biên dịch và truyền mạng cho JavaScript ở mức thấp, có những mẫu có thể giúp ích như phân đoạn dựa trên tuyến đường hoặc PRPL.
PRPL
PRPL (Đẩy, Hiển thị, Lưu trước vào bộ nhớ đệm, Tải từng phần) là một mẫu tối ưu hoá cho tính tương tác thông qua việc phân tách mã và lưu vào bộ nhớ đệm:

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 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ê về lệnh gọi 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 (màu cam) chiếm một phần đáng kể trong thời gian mà nhiều trang web này dành ra:

Wego, một trang web sử dụng PRPL, quản lý để duy trì thời gian phân tích cú pháp thấp cho các tuyến của họ, giúp tương tác rất nhanh. Nhiều trang web khác ở trên đã á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 dần
Nhiều trang web tối ưu hoá khả năng hiển thị nội dung nhưng lại làm giảm khả năng tương tác. Để có được lần hiển thị đầ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 cũng có chi phí riêng. Bạn 1) thường gửi phản hồi HTML lớn hơn có thể đẩy khả năng tương tác của chúng tôi, 2) có thể khiến người dùng rơi vào tình trạng thung lũng kỳ lạ, nơi một nửa trải nghiệm không thể tương tác cho đến khi JavaScript xử lý xong.
Khởi động tăng dần có thể là một phương pháp hay hơn. Gửi xuống 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 từng phần và mở khoá thêm tính năng.
Tải mã tương ứng với nội dung trong khung hiển thị là mục tiêu tối thượng. PRPL và Khởi động tăng 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 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ị liên kết với CPU. Việc giữ cho các chỉ số này ở mức thấp là rất quan trọng.
Các nhóm đã thành công khi áp dụng ngân sách hiệu suất nghiêm ngặt để duy trì thời gian truyền và phân tích cú pháp/biên dịch JavaScript ở mức thấp. Hãy xem bài viết "Can You Afford It?: Ngân sách hiệu suất web thực tế" để được hướng dẫn về ngân sách cho thiết bị di độ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, duy trì thời gian phân tích cú pháp/thời gian biên dịch JavaScript ở mức thấp và áp 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
- Hội nghị Nhà phát triển Chrome 2017 – Các phương pháp hay nhất về tải hiện đại
- Hiệu suất khởi động JavaScript
- Giải quyết khủng hoảng về hiệu suất web — Nolan Lawson
- Bạn có đủ khả năng chi trả không? Ngân sách hiệu suất trong thực tế – Alex Russell
- Đánh giá các khung và thư viện web — Kristofer Baxter
- Kết quả thử nghiệm của Cloudflare với Brotli để nén (lưu ý rằng Brotli động ở chất lượng cao hơn có thể làm chậm quá trình kết xuất trang ban đầu, vì vậy hãy đánh giá cẩn thận. Bạn nên nén tĩnh.)
- Tương lai của hiệu suất — Sam Saccone