Home / Performance / async vs defer: Chọn Cái Nào Để Web Load Nhanh?
Performance

async vs defer: Chọn Cái Nào Để Web Load Nhanh?

async vs defer khác gì nhau? Khi nào dùng cái nào? Hướng dẫn chọn đúng thuộc tính để tối ưu script, tăng tốc web

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 asyncdefer để 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ẽ:

  1. Dừng parse HTML
  2. Tải file script (nếu là external)
  3. Chạy script ngay lập tức
  4. 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:

  1. Trình duyệt parse HTML bình thường
  2. Script tải song song trong background
  3. Khi script tải xong, parse HTML tạm dừng để chạy script
  4. 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áo

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à null

Nế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:

  1. Trình duyệt parse HTML bình thường
  2. Script tải song song trong background
  3. Parse HTML hoàn tất trước
  4. Các script chạy theo đúng thứ tự khai báo
  5. Event DOMContentLoaded kí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 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.jsbootstrap.jsmain.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ínhasyncdefer
Tải scriptSong song (preload scanner)Song songSong song
Parse HTMLBị dừngTiếp tụcTiếp tục
Thực thiNgay khi tải xongNgay khi tải xongSau khi parse HTML xong
Thứ tự thực thiTheo thứ tự khai báoKhông đảm bảoTheo thứ tự khai báo
DOM sẵn sàngKhông đảm bảoKhông đảm bảoĐảm bảo
DOMContentLoadedĐợi scriptKhông đợiĐợi script
Phù hợp choScript inlineScript độc lậpScript 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 --> 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 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 async vì không ảnh hưởng chức năng chính
  • jQuery và Bootstrap dùng defer vì có dependency chain
  • app.js dùng defer để đả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/export syntax
  • 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ả asyncdefer đề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ùng async

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 asyncdefer 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:

  1. Mở DevTools → tab Performance
  2. Bấm RecordReload trang → Stop
  3. 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: DOMContentLoadedLoad. 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:

  • DOMContentLoaded bắ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 ngay
  • load bắn muộn hơn vì phải đợi tất cả resource (images, CSS, scripts) tải xong
  • defer giúp DOMContentLoaded đế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 asyncdefer 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.

← Previous
OOP và Prototypes trong JavaScript hiện đại
Table of Contents
  1. 1. Script Loading Mặc Định (Blocking)
  2. 2. Thuộc Tính async
    1. 2.1. Cách hoạt động
    2. 2.2. Đặc điểm quan trọng
    3. 2.3. Khi nào nên dùng async
  3. 3. Thuộc Tính defer
    1. 3.1. Cách hoạt động
    2. 3.2. Đặc điểm quan trọng
    3. 3.3. Khi nào nên dùng defer
  4. 4. So Sánh async vs defer
    1. 4.1. Bảng so sánh
    2. 4.2. Minh họa trực quan
    3. 4.3. Ví dụ thực tế
  5. 5. Best Practices
    1. 5.1. Đặt script ở đâu?
    2. 5.2. Module scripts
    3. 5.3. Fallback cho trình duyệt cũ
    4. 5.4. Tối ưu performance
    5. 5.5. Lưu ý quan trọng
  6. 6. Đo Lường Performance
    1. 6.1. Performance Tab
    2. 6.2. Network Tab
    3. 6.3. Lighthouse Audit