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, trình duyệt hiện đại vẫn tải song song cả hai nhờ preload scanner (bộ quét trước có sẵn trong trình duyệt, tự động phát hiện resource trong HTML - không phải thẻ <link rel="preload">), nên người dùng chờ khoảng 3 giây - bằng script chậm nhất, không phải 5 giây cộng dồn. Nhưng 3 giây đó parser vẫn đứng yên chờ từng script tải xong và chạy xong, nên nội dung <h1>Welcome</h1> vẫn chưa hiển thị. Cái giá thật sự của blocking script là chặn parse/render, không phải tải tuần tự.
Timeline của script blocking:
View Mermaid diagram code
sequenceDiagram
participant Parser as HTML Parser
participant Scanner as Preload Scanner
participant Screen as Màn hình
Note over Parser: 0s - Parse thẻ head
Scanner->>Scanner: Tải song song analytics.js (2s) + ads.js (3s)
Parser->>Parser: Gặp analytics.js, dừng parse chờ tải
Note over Parser: 2s - analytics.js xong, chạy ngay
Parser->>Parser: Gặp ads.js
Note over Parser: ads.js chưa tải xong, chờ thêm ~1s
Note over Parser: 3s - ads.js xong, chạy
Note over Parser,Screen: 0s đến 3s màn hình vẫn trắng
Parser->>Screen: 3s - Parse tới thẻ body, vẽ h1 "Welcome"
Note over Screen: Người dùng mới thấy chữ "Welcome"Vấn đề chính
- Chặn parse HTML: Parser đứng yên tại thẻ script cho đến khi script tải và chạy xong, chưa kịp đọc tới
<body>để dựng nội dung - Chặn render: Vì parser chưa tới
<body>,<h1>Welcome</h1>không được vẽ - người dùng thấy màn hình trắng suốt thời gian chờ - Render bị khóa sau mọi script: Dù tải song song (nên chờ bằng script tải chậm nhất, không cộng dồn), việc chạy vẫn tuần tự theo thứ tự khai báo; càng nhiều script chặn trong
<head>thì màn hình càng trắng lâu
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 Parser as HTML Parser
participant BG as Tải nền
participant Screen as Màn hình
Note over Parser: 0s - Parse HTML, KHÔNG dừng vì script async
BG->>BG: Tải song song analytics.js (2s) + ads.js (3s)
Parser->>Screen: ~0s - Parse tới thẻ body, vẽ h1 "Welcome"
Note over Screen: Người dùng thấy "Welcome" gần như ngay
Note over BG: 2s - analytics.js xong
BG->>Parser: Dừng để chạy analytics.js rồi parse tiếp
Note over BG: 3s - ads.js xong
BG->>Parser: Dừng để chạy ads.js
Note over Parser: Chạy theo thứ tự TẢI XONG, không theo khai báo2.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 Parser as HTML Parser
participant BG as Tải nền
participant Screen as Màn hình
Note over Parser: 0s - Parse HTML, KHÔNG dừng vì script defer
BG->>BG: Tải song song jquery.js (2s) + main.js (1s)
Parser->>Screen: ~0s - Parse tới thẻ body, vẽ h1 "Welcome"
Note over Screen: Người dùng thấy "Welcome" gần như ngay
Note over Parser: ~0s - Parse HTML xong, nhưng defer scripts vẫn CHỜ
Note over BG: 1s - main.js xong trước nhưng vẫn phải chờ jquery
Note over BG: 2s - jquery.js xong
Parser->>Parser: Chạy jquery.js rồi main.js (đúng thứ tự khai báo)
Note over Parser: Xong hết mới kích hoạt DOMContentLoaded3.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 | Song song (preload scanner) | 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 song song (preload scanner)"]
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 --> End4.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 TRƯỚC 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ũ
Cả async và defer đều đã được mọi trình duyệt còn dùng ngày nay hỗ trợ (phổ cập từ khoảng 2012), nên bạn không cần feature detection. Trình duyệt nào không hiểu thuộc tính sẽ đơn giản bỏ qua nó và chạy script theo kiểu blocking mặc định - trang vẫn hoạt động bình thường, chỉ chậm hơn.
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 script được inject động sau này (parser không tự phát hiện được) -->
<link rel="preload" as="script" href="lazy-widget.js">
<!-- Script chính, dùng defer -->
<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"chỉ hữu ích cho script mà parser không tự phát hiện sớm được (inject động hoặc nằm sâu cuối trang) - script khai báo sẵn trong<head>như defer scripts thì parser đã tự tải sớm rồi- 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
Lý thuyết là một chuyện, nhưng làm sao biết async/defer thực sự giúp trang nhanh hơn? Chrome DevTools cho bạn ba cách để kiểm chứng bằng số liệu thật.
6.1. Performance Tab
Đây là nơi bạn thấy trực quan script chặn parsing như thế nào:
- Mở DevTools → tab Performance
- Bấm Record → Reload trang → Stop
- Xem timeline: một script blocking sẽ hiện ra như một khối dài chắn ngang, đẩy phần render xuống sau. Sau khi thêm
defer, khối đó dịch xuống cuối và trang vẽ nội dung sớm hơn.
Cách nhanh nhất để thấy khác biệt: đo cùng một trang hai lần, một lần với script thường và một lần với defer, rồi so sánh thời điểm nội dung xuất hiện.
6.2. Network Tab
Tab Network cho thấy script tải song song hay tuần tự qua biểu đồ waterfall. Với async/defer, bạn sẽ thấy các script tải chồng lên nhau thay vì xếp hàng nối đuôi.
Hai mốc quan trọng cần để ý là hai đường kẻ dọc trên waterfall: DOMContentLoaded và Load. Bạn có thể log chính xác thời điểm chúng xảy ra:
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:
DOMContentLoadedbắn khi HTML đã parse xong — càng sớm thì trang càng cảm giác nhanh, vì người dùng thấy nội dung ngayloadbắn muộn hơn vì phải đợi tất cả resource (images, CSS, scripts) tải xongdefergiúpDOMContentLoadedđến sớm hơn vì script không còn chặn quá trình parse
6.3. Lighthouse Audit
Nếu bạn muốn một điểm số tổng quát thay vì tự đọc timeline, chạy Lighthouse (DevTools → Lighthouse). Ba chỉ số chịu ảnh hưởng trực tiếp bởi async/defer:
- First Contentful Paint (FCP) — thời điểm nội dung đầu tiên hiện ra
- Time to Interactive (TTI) — khi nào trang phản hồi được thao tác
- Total Blocking Time (TBT) — tổng thời gian main thread bị khóa
Nếu report hiện cảnh báo “Eliminate render-blocking resources”, đó chính là những script đang thiếu 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.