Một thẻ <script> thường (không gắn thuộc tính gì) là kẻ chặn đường: trình duyệt đang đọc HTML, gặp nó là phải dừng dựng trang, tải file về, chạy xong rồi mới đi tiếp. Trang càng nhiều script kiểu này thì người dùng càng phải nhìn màn trắng lâu hơn. Hai thuộc tính defer và async sinh ra để gỡ đúng nút thắt đó, nhưng chúng gỡ theo hai cách rất khác nhau.
Script thường chặn việc dựng trang như thế nào
Khi parser HTML (bộ phân tích cú pháp HTML — thứ đọc file HTML từ trên xuống để dựng cây DOM) gặp một thẻ script không có thuộc tính, nó phải làm ba việc tuần tự: tạm dừng đọc HTML, tải file JavaScript, rồi thực thi. Trong suốt thời gian đó, phần HTML phía dưới chưa được dựng, người dùng chưa thấy gì thêm. Đó là lý do quy tắc cũ “đặt script cuối thẻ body” tồn tại lâu như vậy.
Cả defer lẫn async đều giải quyết phần “tải file”: cho phép trình duyệt tải script song song với việc đọc HTML, không bắt parser ngồi chờ tải. Điểm tách đôi nằm ở phần “thực thi” — khi nào đoạn JavaScript thật sự chạy và có chặn lại lúc đó hay không.
async tải song song, chạy ngay khi sẵn sàng
Với <script async src="...">, file được tải nền trong lúc HTML vẫn đang dựng. Nhưng ngay khi file tải xong, trình duyệt sẽ tạm dừng việc dựng trang để chạy nó luôn. Hệ quả quan trọng:
- Không giữ thứ tự. Nếu trang có ba script async, cái nào tải về trước chạy trước, bất kể vị trí trong HTML. File nhỏ thường vượt mặt file lớn.
- Vẫn có thể chặn việc dựng trang. async không chặn lúc tải, nhưng lúc chạy thì vẫn chiếm main thread (luồng chính — luồng duy nhất xử lý cả dựng giao diện lẫn JavaScript).
Vì không đoán được thứ tự, async chỉ hợp với những script đứng một mình, không phụ thuộc file khác và không bị file khác phụ thuộc: mã đo lường, analytics, pixel quảng cáo, widget bên thứ ba.
defer tải song song, chạy sau khi trang dựng xong
Với <script defer src="...">, file cũng tải nền song song, nhưng việc thực thi bị hoãn đến khi HTML được dựng xong hoàn toàn, ngay trước sự kiện DOMContentLoaded. Hai đặc tính nổi bật:
- Giữ đúng thứ tự khai báo. Nhiều script defer chạy lần lượt theo thứ tự xuất hiện trong HTML, dù file nào tải xong trước. Đây là điểm sống còn khi script B cần biến/hàm do script A tạo ra.
- Không chặn việc dựng trang. Vì hoãn đến cuối, defer gần như không cướp main thread trong giai đoạn người dùng đang chờ trang hiện ra.
defer là lựa chọn an toàn mặc định cho phần lớn JavaScript của trang: thư viện, mã điều khiển giao diện, bất cứ thứ gì cần DOM đã sẵn sàng hoặc cần chạy đúng trình tự.

Bảng so sánh nhanh
| Tiêu chí | Script thường | async | defer |
|---|---|---|---|
| Tải song song với dựng HTML | Không | Có | Có |
| Chặn parser khi đang tải | Có | Không | Không |
| Thời điểm thực thi | Ngay khi gặp | Ngay khi tải xong | Sau khi dựng HTML xong |
| Giữ thứ tự khai báo | Có | Không | Có |
| Hợp với | Hầu như không nên | Mã độc lập (analytics) | Mặc định cho mọi script |
Sơ đồ dòng thời gian
Script thường:
HTML ──[dừng: tải + chạy script]── HTML tiếp ──> xong
async:
HTML ─────────────── HTML ─[dừng: chạy]─ HTML ──> xong
(tải nền) ──────────┘ ngay khi tải xong
defer:
HTML ────────────────────────────── HTML ──> [chạy theo thứ tự]
(tải nền) ─────────────────────────────┘ sau khi dựng xong
Vài quy tắc dễ sai cần nhớ
- Inline script bị bỏ qua. Cả
defervàasyncchỉ có tác dụng khi thẻ cósrc. Gắn vào script viết thẳng trong HTML (khôngsrc) thì trình duyệt phớt lờ. - Gắn cả hai thì async thắng. Nếu một thẻ vừa có
asyncvừa códefer, trình duyệt theoasyncvà bỏ quadefer. - Module hoãn sẵn.
<script type="module">mặc định đã có hành vi như defer, không cần thêmdefer; muốn nó chạy ngay khi tải xong thì gắnasync.
Theo tài liệu MDN, đây là hành vi chuẩn của các trình duyệt hiện đại, không phụ thuộc framework.
Liên hệ với TBT và Core Web Vitals
JavaScript chạy là chiếm main thread; main thread bận quá lâu một lúc thì trang không kịp phản hồi thao tác người dùng. Đó chính là thứ chỉ số TBT đo (Total Blocking Time — tổng thời gian luồng chính bị chặn). Đẩy script không thiết yếu sang defer giúp dồn việc chạy JavaScript về cuối, tránh nghẽn ngay lúc người dùng đang chờ và đang định bấm — qua đó kéo TBT xuống, gián tiếp cải thiện INP (Interaction to Next Paint — độ trễ từ thao tác đến khung hình kế tiếp, ngưỡng tốt ≤ 200ms ở phân vị 75). Muốn hiểu sâu cách TBT phản ánh độ phản hồi, xem bài TBT là gì và cách giảm tổng thời gian chặn.
Lưu ý: defer/async không làm file JavaScript nhỏ đi, chỉ sắp lại thời điểm chạy. Muốn giảm cả khối lượng phải gọt nội dung — gộp và nén file bằng cách minify JS và CSS, hoặc loại mã thừa. Hai việc này bổ trợ nhau: defer dời thời điểm, minify giảm khối lượng.
Câu hỏi thường gặp
Đặt script ở cuối body thì còn cần defer không?
Đặt cuối body giải quyết được phần chặn dựng trang, nhưng defer vẫn tốt hơn vì cho phép tải file sớm và song song ngay khi trình duyệt thấy thẻ trong <head>, lại giữ đúng thứ tự thực thi.
Mã analytics nên dùng defer hay async?
async, vì nó độc lập, không phụ thuộc script khác và càng chạy sớm càng đo đủ. Còn thư viện hay mã điều khiển giao diện thì để defer cho an toàn thứ tự.
Gắn defer mà script báo lỗi “không tìm thấy hàm” thì sao?
Thường do một script khác phụ thuộc nó nhưng lại để async nên chạy trước. Chuyển tất cả script phụ thuộc nhau sang cùng defer để giữ thứ tự.
Nếu trang của bạn có nhiều script bên thứ ba đang kéo điểm tốc độ xuống và bạn không chắc cái nào nên defer, cái nào async, Web22 có dịch vụ tối ưu tốc độ website rà soát giúp.
