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

  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 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à 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 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.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 scriptBlockingSong 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 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 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ũ

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ế, asyncdefer đượ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ù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

Để xác minh hiệu quả của async/defer, dùng Chrome DevTools:

#6.1. Performance Tab

  1. Mở DevTools → Performance tab
  2. Bấm Record → Reload page → Stop
  3. 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:

  • DOMContentLoaded nhanh hơn = website cảm giác nhanh hơn
  • load event đợ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 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.