async vs defer: Chọn Cái Nào Để Web Load Nhanh?
Bạn mở trang web và thấy màn hình trắng xóa vài giây trước khi nội dung xuất hiện? Đó là dấu hiệu script đang chặn (blocking) việc render - trình duyệt phải dừng parse HTML, tải và chạy script xong mới tiếp tục hiển thị.
Website hiện đại thường load hàng chục file JavaScript từ analytics, ads, libraries nên vấn đề này càng nghiêm trọng. May mắn là HTML5 cung cấp async và defer để kiểm soát cách script được tải, giúp web load nhanh hơn đáng kể.
#1. Script Loading Mặc Định (Blocking)
Khi bạn thêm script vào HTML mà không có thuộc tính gì, trình duyệt sẽ hoạt động theo cách blocking:
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<script src="analytics.js"></script>
<script src="ads.js"></script>
</head>
<body>
<h1>Welcome</h1>
<p>This is my website</p>
</body>
</html>Giải thích:
Khi trình duyệt gặp thẻ <script>, nó sẽ:
- Dừng parse HTML
- Tải file script (nếu là external)
- Chạy script ngay lập tức
- Mới tiếp tục parse phần HTML còn lại
Trong thực tế, nếu analytics.js mất 2 giây để tải và ads.js mất 3 giây, người dùng phải chờ 5 giây mới thấy nội dung <h1>Welcome</h1>.
Timeline của script blocking:
View Mermaid diagram code
sequenceDiagram
participant Browser
participant HTML
participant Script1
participant Script2
Browser->>HTML: Bắt đầu parse HTML
HTML->>Script1: Gặp script thứ nhất
Note over Browser: Dừng parse HTML
Browser->>Script1: Tải analytics.js
Browser->>Script1: Chạy analytics.js
Note over Browser: Tiếp tục parse HTML
HTML->>Script2: Gặp script thứ hai
Note over Browser: Dừng parse HTML lại
Browser->>Script2: Tải ads.js
Browser->>Script2: Chạy ads.js
Note over Browser: Hoàn thành parse HTML
Browser->>HTML: Hiển thị nội dung#Vấn đề chính
- Blocking render: Nội dung không hiển thị cho đến khi script chạy xong
- Tải tuần tự: Các script phải tải xong lần lượt
- Trải nghiệm người dùng kém: Màn hình trắng kéo dài
#2. Thuộc Tính async
Thuộc tính async cho phép script tải song song với việc parse HTML và chạy ngay khi tải xong.
#2.1. Cách hoạt động
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<script async src="analytics.js"></script>
<script async src="ads.js"></script>
</head>
<body>
<h1>Welcome</h1>
<p>This is my website</p>
</body>
</html>Giải thích:
Với async:
- Trình duyệt parse HTML bình thường
- Script tải song song trong background
- Khi script tải xong, parse HTML tạm dừng để chạy script
- Sau khi script chạy xong, tiếp tục parse HTML
Timeline của async:
View Mermaid diagram code
sequenceDiagram
participant Browser
participant HTML
participant Script1
participant Script2
Browser->>HTML: Bắt đầu parse HTML
par Tải song song
Browser->>Script1: Tải analytics.js (background)
and
Browser->>Script2: Tải ads.js (background)
and
Browser->>HTML: Parse HTML tiếp tục
end
Note over Script2: ads.js tải xong trước
Browser->>HTML: Tạm dừng parse
Browser->>Script2: Chạy ads.js
Browser->>HTML: Tiếp tục parse
Note over Script1: analytics.js tải xong sau
Browser->>HTML: Tạm dừng parse
Browser->>Script1: Chạy analytics.js
Browser->>HTML: Hoàn thành parse#2.2. Đặc điểm quan trọng
#Thứ tự thực thi không đảm bảo
Script nào tải xong trước sẽ chạy trước, không theo thứ tự khai báo trong HTML.
1
2
<script async src="jquery.js"></script>
<script async src="main.js"></script> <!-- Phụ thuộc vào jQuery -->Trong ví dụ này, main.js có thể chạy trước jquery.js nếu nó tải xong trước, gây lỗi $ is not defined.
#Không đợi DOMContentLoaded
Script async có thể chạy trước hoặc sau khi DOM sẵn sàng:
1
2
// analytics.js with async
console.log(document.querySelector('h1')); // Có thể là nullNếu script chạy trước khi parse đến thẻ <h1>, kết quả sẽ là null.
#2.3. Khi nào nên dùng async
Dùng async cho:
- Script độc lập, không phụ thuộc vào script khác
- Script không cần thao tác với DOM
- Analytics, tracking, ads scripts
- Third-party scripts không quan trọng cho nội dung chính
1
2
3
4
<!-- Các trường hợp phù hợp -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<script async src="https://platform.twitter.com/widgets.js"></script>Giải thích:
Những script này hoạt động độc lập, không ảnh hưởng đến chức năng chính của trang. Nếu chúng load chậm hoặc fail, website vẫn hoạt động bình thường.
#3. Thuộc Tính defer
Thuộc tính defer tải script song song nhưng đợi đến khi HTML parse xong mới chạy.
#3.1. Cách hoạt động
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html>
<head>
<title>My Website</title>
<script defer src="jquery.js"></script>
<script defer src="main.js"></script>
</head>
<body>
<h1>Welcome</h1>
<p>This is my website</p>
</body>
</html>Giải thích:
Với defer:
- Trình duyệt parse HTML bình thường
- Script tải song song trong background
- Parse HTML hoàn tất trước
- Các script chạy theo đúng thứ tự khai báo
- Event
DOMContentLoadedkích hoạt sau khi script chạy xong
Timeline của defer:
View Mermaid diagram code
sequenceDiagram
participant Browser
participant HTML
participant Script1
participant Script2
participant DOM
Browser->>HTML: Bắt đầu parse HTML
par Tải song song
Browser->>Script1: Tải jquery.js (background)
and
Browser->>Script2: Tải main.js (background)
and
Browser->>HTML: Parse HTML tiếp tục
end
Browser->>HTML: Parse HTML hoàn tất
Note over Browser: Đợi tất cả defer scripts
Browser->>Script1: Chạy jquery.js (theo thứ tự)
Browser->>Script2: Chạy main.js (theo thứ tự)
Browser->>DOM: Kích hoạt DOMContentLoaded#3.2. Đặc điểm quan trọng
#Thứ tự thực thi được đảm bảo
Script luôn chạy theo thứ tự khai báo, giống như script thông thường nhưng không blocking:
1
2
3
<script defer src="jquery.js"></script>
<script defer src="bootstrap.js"></script> <!-- Phụ thuộc jQuery -->
<script defer src="main.js"></script> <!-- Phụ thuộc cả hai -->Thứ tự chạy: jquery.js → bootstrap.js → main.js, an toàn cho dependencies.
#DOM đã sẵn sàng
Khi script defer chạy, DOM đã parse xong, bạn có thể truy cập bất kỳ element nào:
1
2
3
// main.js with defer
console.log(document.querySelector('h1')); // Luôn có giá trị
document.querySelector('h1').textContent = 'Updated Title';Không cần DOMContentLoaded event listener vì DOM đã sẵn sàng.
#3.3. Khi nào nên dùng defer
Dùng defer cho:
- Script cần thao tác với DOM
- Script có dependencies với script khác
- Script chức năng chính của website
- Libraries và frameworks
1
2
3
4
<!-- Các trường hợp phù hợp -->
<script defer src="jquery-3.7.1.min.js"></script>
<script defer src="bootstrap.bundle.min.js"></script>
<script defer src="app.js"></script>Giải thích:
Các script này cần chạy theo thứ tự và cần DOM sẵn sàng. defer đảm bảo cả hai điều kiện này mà vẫn không blocking parse HTML.
#4. So Sánh async vs defer
#4.1. Bảng so sánh
| Tiêu chí | Không có thuộc tính | async | defer |
|---|---|---|---|
| Tải script | Blocking | Song song | Song song |
| Parse HTML | Bị dừng | Tiếp tục | Tiếp tục |
| Thực thi | Ngay khi tải xong | Ngay khi tải xong | Sau khi parse HTML xong |
| Thứ tự thực thi | Theo thứ tự khai báo | Không đảm bảo | Theo thứ tự khai báo |
| DOM sẵn sàng | Không đảm bảo | Không đảm bảo | Đảm bảo |
| DOMContentLoaded | Đợi script | Không đợi | Đợi script |
| Phù hợp cho | Script inline | Script độc lập | Script phụ thuộc nhau |
#4.2. Minh họa trực quan
View Mermaid diagram code
flowchart TD
Start([Bắt đầu load trang]) --> Question{Script có thuộc tính gì?}
Question -->|Không có| Block[Parse HTML bị dừng]
Block --> LoadSync[Tải script blocking]
LoadSync --> RunSync[Chạy script ngay]
RunSync --> ContinueParse1[Tiếp tục parse HTML]
Question -->|async| ParseAsync[Parse HTML tiếp tục]
ParseAsync --> LoadAsync[Tải script song song]
LoadAsync --> RunAsync[Chạy ngay khi tải xong]
RunAsync --> ContinueParse2[Tiếp tục parse HTML]
Question -->|defer| ParseDefer[Parse HTML tiếp tục]
ParseDefer --> LoadDefer[Tải script song song]
LoadDefer --> WaitHTML[Đợi HTML parse xong]
WaitHTML --> RunDefer[Chạy theo thứ tự]
ContinueParse1 --> End([Trang load xong])
ContinueParse2 --> End
RunDefer --> End#4.3. Ví dụ thực tế
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html>
<head>
<title>E-commerce Website</title>
<!-- Analytics - độc lập, dùng async -->
<script async src="https://www.google-analytics.com/analytics.js"></script>
<!-- Libraries có dependencies - dùng defer -->
<script defer src="jquery-3.7.1.min.js"></script>
<script defer src="bootstrap.bundle.min.js"></script>
<script defer src="app.js"></script>
<!-- Ads - độc lập, dùng async -->
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
</head>
<body>
<nav class="navbar">...</nav>
<main>
<h1>Products</h1>
<div id="product-list"></div>
</main>
<!-- Script inline chạy sau defer scripts -->
<script>
// Code này chạy trước app.js vì không có defer
console.log('Inline script');
</script>
</body>
</html>Giải thích:
- Analytics và ads dùng
asyncvì không ảnh hưởng chức năng chính - jQuery và Bootstrap dùng
defervì có dependency chain app.jsdùngdeferđể đảm bảo chạy sau jQuery/Bootstrap và DOM sẵn sàng- Script inline chạy ngay khi gặp, không chờ defer scripts
#5. Best Practices
#5.1. Đặt script ở đâu?
#Trong <head> với defer hoặc async
1
2
3
4
<head>
<script defer src="main.js"></script>
<script async src="analytics.js"></script>
</head>Lợi ích: Trình duyệt biết sớm có script nào cần tải, bắt đầu download ngay.
#Cuối <body> nếu không dùng defer/async
1
2
3
4
<body>
<!-- Nội dung HTML -->
<script src="main.js"></script>
</body>Lợi ích: HTML đã parse xong, script không blocking render.
#5.2. Module scripts
Với ES6 modules, type="module" tự động có behavior giống defer:
1
<script type="module" src="app.js"></script>Giải thích:
- Module scripts luôn defer by default
- Hỗ trợ
import/exportsyntax - Chạy trong strict mode
- Có scope riêng (không pollute global)
Nếu muốn async cho module:
1
<script type="module" async src="analytics.js"></script>#5.3. Fallback cho trình duyệt cũ
Trình duyệt cũ không hỗ trợ async/defer sẽ bỏ qua thuộc tính và chạy blocking. Kiểm tra browser support:
1
2
3
4
// Check defer support
if ('defer' in document.createElement('script')) {
console.log('Browser supports defer');
}Trong thực tế, async và defer được hỗ trợ từ IE10+, hầu hết trình duyệt hiện đại đều hỗ trợ.
#5.4. Tối ưu performance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<head>
<!-- Critical CSS inline -->
<style>
/* CSS quan trọng cho above-the-fold */
</style>
<!-- Preload critical scripts -->
<link rel="preload" as="script" href="critical.js">
<!-- Defer non-critical -->
<script defer src="critical.js"></script>
<script defer src="main.js"></script>
<!-- Async for third-party -->
<script async src="analytics.js"></script>
</head>Giải thích:
rel="preload"bảo trình duyệt tải script với priority cao- Critical CSS inline để tránh blocking render
- Script quan trọng dùng
defer, third-party dùngasync
#5.5. Lưu ý quan trọng
#Inline scripts không hỗ trợ async/defer
1
2
3
4
<!-- Không hoạt động - inline script luôn blocking -->
<script async>
console.log('Vẫn chạy ngay, không async');
</script>Giải thích:
Thuộc tính async và defer chỉ hoạt động với external scripts (có src). Inline scripts luôn:
- Chạy ngay khi trình duyệt gặp
- Block HTML parsing
- Phải đợi CSS phía trên parse xong (vì có thể query styles)
Best practice: Giữ inline scripts tối thiểu, chuyển code sang external file với defer. Để hiểu sâu hơn về cách inline scripts ảnh hưởng đến rendering, xem bài Critical Rendering Path.
#Script có document.write() không nên dùng async
1
2
// script.js - Không nên dùng async
document.write('<p>Added by script</p>');document.write() trong async script có thể gây lỗi vì HTML đã parse xong.
#Kết hợp async và defer
1
2
<!-- Browser chỉ chọn một trong hai -->
<script async defer src="script.js"></script>Nếu cả hai thuộc tính đều có, trình duyệt ưu tiên async. Chỉ dùng một trong hai cho rõ ràng.
#6. Đo Lường Performance
Để xác minh hiệu quả của async/defer, dùng Chrome DevTools:
#6.1. Performance Tab
- Mở DevTools → Performance tab
- Bấm Record → Reload page → Stop
- Xem timeline để thấy:
- Script blocking: Vùng vàng dài
- Parse HTML: Vùng tím
- Script execution: Vùng xám
#6.2. Network Tab
Kiểm tra:
- Waterfall: Script nào tải song song
- DOMContentLoaded: Dòng xanh dương
- Load: Dòng đỏ
1
2
3
4
5
6
7
8
// Đo thời gian DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM ready at:', performance.now(), 'ms');
});
window.addEventListener('load', function() {
console.log('Page fully loaded at:', performance.now(), 'ms');
});Giải thích:
DOMContentLoadednhanh hơn = website cảm giác nhanh hơnloadevent đợi tất cả resources (images, css, scripts)
#6.3. Lighthouse Audit
Chạy Lighthouse (DevTools → Lighthouse) để kiểm tra:
- First Contentful Paint (FCP)
- Time to Interactive (TTI)
- Total Blocking Time (TBT)
Recommendations thường bao gồm “Eliminate render-blocking resources” - chính là script cần thêm async/defer.
Để hiểu sâu hơn về cách async/defer ảnh hưởng đến quá trình render trang web, xem bài viết Critical Rendering Path: Hành Trình Từ HTML Đến Pixels.
Vậy là bạn đã hiểu cách async và defer hoạt động và khi nào nên dùng từng loại. Trong thực tế, hãy dùng defer cho script chính của website và async cho third-party scripts như analytics. Điều này giúp website load nhanh hơn mà vẫn đảm bảo chức năng hoạt động đúng.