Categories Tối ưu JavaScript

Tải JavaScript hiệu quả với defer và async

Khi bạn tải JavaScript trên trang HTML, bạn cần cẩn thận để không làm ảnh hưởng xấu đến hiệu suất (tốc độ) của trang. Phụ thuộc vào việc bạn đặt đoạn mã JavaScript ở đâucách đặt nó trên trang HTML, sự kết hợp này sẽ ảnh hưởng đến thời gian tải trang (loading time).

Một đoạn mã JavaScript thông thường sẽ được đặt như thế này:

<script src="script.js"></script>

Bất cứ khi nào trình phân tích (parser) HTML tìm thấy dòng này, một yêu cầu sẽ được thực hiện để tìm nạp (fetch / tải xuống) JavaScript, và đoạn mã sẽ được thực thi (executed).

Một khi quá trình này hoàn thành, trình phân tích cú pháp mới có thể tiếp tục (resume), và phần còn lại của HTML (có thể) được phân tích tiếp [như cuộc chạy tiếp sức, khi từng người chỉ được chạy khi đến lượt của họ, diễn ra vào lúc trao gậy].

Như bạn có thể đoán được rồi, kiểu hoạt động này có thể ảnh hưởng lớn lên thời gian tải của trang.

Nếu JavaScript mất nhiều thời gian hơn để tải so với dự định, ví dụ nếu mạng hơi chậm hoặc nếu trên thiết bị di động và kết nối không ổn định (sloppy), người xem có khả năng thấy một trang trống trơn (blank page) cho đến khi đoạn mã được tải và thực thi.


Các vị trí quan trọng

Khi bạn mới học HTML, bạn đưa thẻ JavaScript vào trong thẻ <head> như dưới đây:

<html>
  <head>
    <title>Tiều đề</title>
    <script src="script.js"></script>
  </head>
  <body>
    ...
  </body>
</html>

Như tôi nói ở phần trên, khi trình phân tích tìm thấy dòng này, nó sẽ tìm nạp đoạn mã và thực thi mã. Sau đó, khi trình phân tích làm xong nhiệm vụ này, nó sẽ tiếp tục phân tích phần thân (body).

Điều này là tệ bởi vì nó là nguyên nhân gây trì hoãn, chậm trễ (delay). Một giải pháp phổ biến cho vấn đề này là đưa thẻ <script> vào phần cuối của trang, ngay trước thẻ đóng </body>.

Nhờ vậy, JavaScript được tải về và thực thi (loaded and executed) sau khi toàn bộ trang đã sẵn sàng phân tích và tải xong, điều này là một cải tiến lớn so với việc đưa mã vào thẻ head.

Đây là cách tốt nhất mà chúng ta có thể thực hiện nếu bạn cần hỗ trợ cho các trình duyệt cũ (older browsers) không hỗ trợ hai tính năng mới của HTML là: asyncdefer.


Async và Defer

Cả asyncdefer là các thuộc tính boolean. Cách sử dụng của chúng là tương tự nhau:

<script async src="script.js"></script>
<script defer src="script.js"></script>

Nếu bạn chỉ định cả hai, async sẽ được ưu tiên trên các trình duyệt hiện đại, trong khi với các trình duyệt cũ hỗ trợ defer nhưng không hỗ trợ async thì nó sẽ được chuyển thành defer.

Để kiểm tra các phiên bản trình duyệt hỗ trợ async, bạn có thể kiểm tra thông qua trang Can I Use: https://caniuse.com/#feat=script-async còn đây là cho thuộc tính defer: https://caniuse.com/#feat=script-defer

Các thuộc tính này chỉ có ý nghĩa khi sử dụng JavaScript trong phần head của trang, và chúng sẽ vô tác dụng nếu bạn để mã trong phần cuối của body như chúng ta đã thấy ở trên.


So sánh hiệu suất

1. Không có defer hoặc async, nằm trong thẻ head

Dưới đây là cách trang tải mã mà không có cả defer hoặc async, khi nó được đặt trong thẻ head của trang:

không có async và defer
  • start parsing HTML: bắt đầu phân tích HTML;
  • wait: đợi;
  • fetch script: tìm nạp mã JS;
  • excute script: thực thi mã JS;
  • resume parsing HTML: tiếp tục phân tích cú pháp HTML;

Quá trình phần tích HTML dừng lại cho đến khi mã JS được tìm nạp và thực thi xong. Khi quá trình này hoàn thành, quá trình phân tích HTML mới lại tiếp tục.

Tóm lại: với cách này JS sẽ làm gián đoạn phân tích HTML trong cả 2 pha là tìm nạp & thực thi. Nói cách khác nó làm website bị chậm đi nhiều nhất.


2. Không defer hoặc async, nằm trong thẻ body

Dưới đây là cách trang tải mã mà không có defer hoặc async, đặt tại phần cuối của thẻ body, ngay trước thẻ đóng:

không có defer và async trong body

Quá trình phân tích được thực hiện mà không bị tạm dừng lần nào trong suốt giai đoạn này (parse HTML), và khi nó hoàn thành, tiếp theo JS sẽ được tìm nạp, và thực thi. Phân tích cú pháp được hoàn thành thậm chí là trước khi JS tải về, vì thế trang sẽ xuất hiện trước cách trên.

Với cách này cả 2 pha tìm nạp & thực thi không cản trở quá trình phân tích HTML.


3. Với async, nằm trong thẻ head

Dưới đây là cách trang tải JavaScript với async, ở trong thẻ head:

với async trong thẻ head

Đoạn mã JavaScript được tìm nạp không đồng bộ (asynchronously), và khi nó sẵn sàng, trình phân tích sẽ tạm dừng để thực thi JavaScript, sau khi hoàn thành, nó lại tiếp tục trở lại.

Với cách này chỉ pha thực thi cản trở phân tích HTML, còn quá trình tìm nạp thì không, nó được thực hiện song song với phân tích HTML.


4. Với defer trong head

Dưới đây là cách trang tải mã với defer, được đặt trong thẻ head:

defer trong thẻ head

Đoạn mã được tìm nạp không đồng bộ, và nó được thực thi chỉ khi trình phân tích làm việc xong với HTML.

Trình phân tích hoàn thành việc này cũng giống như khi chúng ta để JS tại phần cuối của thẻ body, nhưng nói chung mã được thực thi sớm hơn, bởi vì đoạn mã được tải song song với quá trình phân tích HTML.

Vì thế đây là giải pháp tốt nhất khi xét về mặt tốc độ.


Kết luận: để defer JS trong thẻ head là cách tốt nhất về mặt tốc độ, cách tốt thứ nhì là để trước thẻ đóng body.


Giải thích dễ hiểu hơn: chạy tiếp sức không phải là ví dụ tốt để minh họa phần này. Tôi sẽ lấy ví dụ khác. Hãy tưởng tượng bạn phải làm 2 món ăn, một món chính và một món ăn phụ. Món chính là lem rán (parse HTML), và món phụ là hoa quả tráng miệng. Tuy nhiên trước khi gọt (execute) hoa quả thì bạn phải ngâm (fetch) cho sạch. Thế thì cách làm chậm chạp là làm món chính, làm xong thì mới ngâm hoa quả cho sạch, đợi ngâm xong mới gọt hoa quả. Cách làm nhanh hơn là trong khi bạn làm món chính, hoa quả được ngâm (chắc chắn bạn làm việc này nhiều rồi đúng không!), quá trình diễn ra song song, và làm xong món lem rán thì bạn gọt được luôn hoa quả chứ không phải chờ thêm thời gian để ngâm nữa.


Chặn phân tích cú pháp

async chặn trình phân tích trang trong khi defer không làm như vậy.


Chặn hiển thị

Cả async lẫn defer đều không đảm bảo bất cứ điều gì về chặn hiển thị (blocking rendering). Điều này phụ thuộc vào bạn và mã của bạn (ví dụ, đảm bảo rằng mã JavaScript của bạn chạy sau onLoad).


domInteractive

JS được đánh dấu với defer sẽ được thực thi ngay sau sự kiện domInteractive, cái xảy ra sau khi HTML tải, phân tích và xây dựng DOM.

CSS và ảnh tại thời điểm này sẽ vẫn được phân tích cú pháp và tải.

Một khi điều này hoàn thành xong, trình duyệt sẽ khởi phát sự kiện dotComplete, và sau đó là onLoad.

domInteractive là quan trọng bởi vì thời gian của nó được ghi nhận là thước đo tốc độ tải nhận thức (perceived loading speed). Xem thêm ở đây để biết chi tiết hơn.


Giữ mọi thứ theo thứ tự

Một điểm cần lưu ý về ưu điểm của defer: JavaScript được đánh dấu với async được thực thi ngay khi chúng sẵn sàng. Còn JavaScript được đánh dấu defer được thực thi (sau khi trình phân tích cú pháp HTML hoàn thành) theo thứ tự mà nó được xác định từ trước trong mã đánh dấu HTML (do đó nó hạn chế khả năng gây ra lỗi do không làm đảo lộn trật tự thực thi mã của người lập trình).


Hãy nói cho tôi biết cách tốt nhất!

Cách tốt nhất để tăng tốc trang của bạn khi sử dụng JavaScript là đưa chúng vào trong phần <head> và thêm thuộc tính defer vào thẻ <script>:

<script defer src="script.js"></script>

Đây là kịch bản giúp kích hoạt sự kiện domInteractive nhanh hơn.

Hãy để ý đến ưu điểm của defer, nó dường như là lựa chọn tốt hơn async trong nhiều bối cảnh đa dạng.

Trừ khi bạn ổn thỏa với việc trì hoãn trong lần render đầu tiên của trang, còn không hãy đảm bảo rằng khi trang được phân tích cú pháp, JavaScript bạn muốn đã được thực thi (câu này có ý nói rằng nếu bạn cần JS thực thi trong khi phân tích trang thì async mới là lựa chọn tốt hơn, vì nó không bị trì hoãn như với defer, một ví dụ điển hình trong vấn đề này là jQuery, nhiều trang sẽ gặp lỗi khi defer jQuery).

(Dịch từ bài viết: Efficiently load JavaScript with defer and async, website: flaviocopes)

Back to Top