Dạo gần đây mình đang thực hiện việc optimize load trang cho một trang web cũ kỹ, còn xài jQuery là chính. Tuy nói cũ kỹ nhưng khi tiến hành optimize thì mới lộ ra vài thứ cũng đáng học hỏi. Một trong số đó là đảm bảo làm sao việc load trang ở ngay lúc đầu user truy cập, nhanh và mượt nhất có thể.
Để nhanh và mượt, ngoài việc minify resources (html, js, css, …) để giảm dung lượng file và tải nhanh hơn, mình còn check xem liệu nó có đặt thẻ <script>
đúng vị trí và xài attribute async
và defer
hay chưa! Vâng chính xác, đúng vị trí thẻ script cũng ảnh hưởng đến tốc độ load của trang web đó. Nào cùng mình tìm hiểu xem vì sao lại như vậy nhé.
Lưu ý đầu tiên
Có vài điều cần nắm trước khi tiếp tục đó là:
- Chúng ta cần biết khi load một trang web, cái browser kia sẽ thực hiện những bước như thế nào:
- User gửi request, và browser tiếp nhận, tiến hành fetch (load về) file html và tiến hành phân tích (parse).
- Khi parse file html nó sẽ “lấy ra” danh sách resource (scripts, stylesheets) trong đó ra.
- Sau đó tiến hành load những resource này về. Đối với load resource là JS, khi load về thì đồng thời ngừng việc hiển thị DOM lên trình duyệt.
- Khi load xong resource css, thì nó tiến hành apply css này vào cây DOM (apply css kiểu như là mặc áo cho trang web).
- Còn với resource js, sau khi load xong (tạo ra cây DOM hoàn chỉnh), browser tiến hành thực thi những file này, và hoàn tất việc load một trang web ra cho người dùng xem.
- Thông thường thẻ script được đặt ở ba nơi, trong thẻ
<head>
hoặc trong thẻ<body>
hoặc bên ngoài dưới thẻ<body>
. Và vị trí đặt thẻ<script>
sẽ ảnh hưởng tới việc load về và thực thi của chúng.
Bất cập khi đặt thẻ <script>
bên trong thẻ <body>
1 | <html> |
Ngày xưa, khi mới học JS chắc hẳn phương pháp đặt thẻ <script>
bên trong thẻ <body>
là phổ biến nhất, nhưng bây giờ thì phương pháp đó không còn đúng nữa. Nghĩa là không nên đặt thẻ <script>
bên trong thẻ <body>
.
Bởi vì khi đặt trong thẻ body, quá trình parse DOM sẽ block lại đợi cho load resource về, thực thi resource rồi nó mới hoàn thành việc tạo DOM. Có nghĩa nếu bạn để cả mớ thẻ <script>
thì lúc load, user sẽ thấy trang web load load và trắng trơn, chỉ nào js xong xuôi nó mới hiển thị lên màn hình.
- Đấy là cái cảnh mình thấy ở trang web mình đang optimize, chờ một trang trắng lâu khiến mình thấy trang web kia như sh**.
Vậy thì đặt ở đâu? Bên ngoài dưới thẻ <body>
. Uhm hư, nó cũng như nhau khi đặt trong thẻ body thôi. Vậy câu trả lời chuẩn nhất đấy là đặt ở bên trong thẻ <head>
.
Đặt thẻ <script>
bên trong thẻ <head>
1 | <html> |
Về cơ bản nó chỉ là cut code bên dưới dán lên bên trên thôi đúng không. Nhưng lúc này cơ chế hoạt động sẽ khác đi đôi chút. Khi bạn đặt thẻ <script>
trong thẻ <head>
, browser sẽ load và thực thi file JS cùng lúc khi cây DOM được tạo ra.
Chỗ này sẽ nảy sinh ra một vấn đề đấy là: trong file JS kia mình “muốn” tác động đến cây DOM, mà cây DOM chưa được tạo ra lấy gì mà tác động?
Để khắc phục điều này, chúng ta cần đợi khi “dựng” cây DOM xong thì mới thực thi JS, để làm được điều này chúng ta có nhiều cách.
Cách cũ, dùng event DOMContentLoaded
Với cách này, chúng ta lắng nghe event DOMContentLoaded
và truyền vào callback để khi cây DOM load xong thì mới thực thi code JS:
1 | document.addEventListener('DOMContentLoaded', function () { |
Event này được trình duyệt gọi khi mà cây DOM đã dựng xong.
Cách mới, dùng attribute async hoặc defer
Đây là hai thuộc tính của thẻ <script>
hỗ trợ việc load file JS độc lập ở thread riêng, tránh việc block main thread. Vậy câu hỏi đặt ra, khi nào dùng async
, khi nào dùng defer
, async
và defer
khác gì nhau? Oke mình sẽ giải thích ngay và luôn.
1. async
1 | <html> |
Thuộc tính async
đảm bảo các tiêu chí:
- Thuộc tính này đảm bảo việc load file JS độc lập ở thread riêng, không ảnh hưởng gì đến việc tạo DOM của main thread. Tức là ở bước phân tích (parse) HTML, tách ra các thẻ
<script>
thì đồng thời đi load JS luôn. - Việc tạo DOM và hoàn tất hiển thị lên màn hình không bị ngừng lại do việc load JS.
- Khi mà load JS xong, thì nó sẽ ngừng việc tạo DOM và tiến hành thực thi JS. Nó chỉ tránh block khi load JS mà thôi.
- Với nhiều thẻ
<script>
nó sẽ load và thực thi bất đồng bộ với nhau, không quan tâm thứ tự ai đặt trước, đặt sau.
1 | <html> |
2. defer
1 | <html> |
Thuộc tính defer
đảm bảo các tiêu chí:
- Cũng giống như
async
,defer
đảm bảo việc load file JS độc lập ở thread riêng, không ảnh hưởng gì đến việc tạo DOM của main thread. - Nhưng không giống
async
thực thi JS ngay sau khi load xong,defer
sẽ “tạm gác” việc thực thi, đợi khi nào tạo cây DOM xong, thì mới tiến hành thực thi. - Điều đó đồng nghĩa với việc, từ đầu tới cuối nó không block việc tạo DOM.
- Thực thi sẽ theo thứ tự đặt thẻ
<script>
, chứ không nhưasync
, thực thi bất đồng bộ giữa các thẻ<script>
1 | <html> |
Khi nào dùng async khi nào defer
Dùng async khi:
- Khi mà code của các thẻ
<script>
chúng không phụ thuộc lẫn nhau. Code của mỗi thẻ<script>
chạy độc lập nhau. - Khi mà code JS của bạn không đả động gì tới cây DOM.
Dùng defer khi:
- Khi mà code của các thẻ
<script>
phụ thuộc lẫn nhau, cần chạy cái này trước khi chạy cái kia, ví dụ, bootstrap cần load và chạy jquery trước khi nó thực thi. - Khi mà code có đả động tới cây DOM.
1 | <head> |
Thứ tự thực thi sẽ là jquery
, sau đó bootstrap
và cuối cùng là app
. async
thì không bận tâm thứ tự, ai trước cũng được, vì bất đồng bộ mà.
Tóm lại
- Ngày nay việc quan tâm đến dùng hai thuộc tính này không còn quá quan trọng nữa, khi mà library, framework đã có tool, cli, … hỗ trợ gen ra rồi, chả còn mấy ai để ý tới nữa, trừ mấy trang web củ chuối :v.
- Hạn chế việc đặt thẻ
<script>
bên trong thẻ body. - Nếu có nhiều thẻ
<script>
nên ưu tiên đặt ở trong thẻ head. async
vàdefer
đúng theo nhu cầu. Cả hai đều không block việc tạo DOM.async
khi không quan tâm thứ tự thực thi JS,defer
thì theo thứ tự mà thực thi trước sau.- Ảnh dưới mô phỏng cách chạy của
async
vàdefer
:

Nguồn ảnh từ HTML spec, copy và chế lại dự trên giấy phép nội dung CC BY 4.0. (trích dẫn từ MDN docs)