Scripts: async and defer Β· Astro Tech Blog

Scripts: async and defer

How you load your scripts impacts page performance. The async and defer attributes give you control over when scripts download and execute.

The Problem

A regular <script> tag blocks HTML parsing while it downloads and executes:

<script src="heavy.js"></script>
<!-- HTML parsing stops here until heavy.js downloads and runs -->

This is why putting scripts in <head> without defer or async slows down page load.

The Three Modes

AttributeDownloadExecutionOrder
(none)Blocks HTML parsingImmediately when downloadedIn document order
asyncNon-blockingAs soon as downloadedNo guarantee
deferNon-blockingAfter HTML parsed, before DOMContentLoadedIn document order
Normal:     HTML ─────── script β”œβ”€β”€β”€β”€β”€β”€ HTML ──────▢
                
Async:      HTML ───────────── script β”œβ”€β”€β–Ά
            (script downloads in parallel)

Defer:      HTML ───────────────────── script β”œβ”€β”€β–Ά
            (script runs after HTML parsed, before DOMContentLoaded)

defer β€” Safe, Ordered Execution

defer tells the browser: β€œDownload the script, but wait until HTML is parsed to execute it.”

<script defer src="analytics.js"></script>
<script defer src="app.js"></script>
  • Both download in parallel during HTML parsing
  • Both execute in order (analytics.js first, then app.js)
  • Both execute before DOMContentLoaded

Use defer for scripts that need the DOM and depend on each other.

async β€” Independent Execution

async tells the browser: β€œDownload as soon as possible, run as soon as downloaded.”

<script async src="analytics.js"></script>
<script async src="widget.js"></script>
  • Both download in parallel
  • Each executes immediately when downloaded
  • Order is not guaranteed (analytics may run after widget)

Use async for independent scripts (analytics, ads, counters) that don’t depend on the DOM or other scripts.

Visual Comparison

Normal script in <head>:
Parsing ───scriptβ”œβ”€β”€β–Ά DOMContentLoaded ──▢ load
            (blocked)

Deferred script:
Parsing ────────────────▢ DOMContentLoaded ──▢ load
            β”œβ”€β”€defer──▢  (after parse)

Async script:
Parsing ──────▢ DOMContentLoaded ──▢ load
     β”œasync─   (when ready)

When to Use Which

ScenarioBest Choice
Script uses DOM APIsdefer
Script depends on another scriptdefer (order preserved)
Analytics / trackingasync
Third-party widgetsasync
Small inline scriptInline at end of <body>
Script doesn’t need DOMasync

Dynamic Script Loading

You can also load scripts dynamically:

const script = document.createElement('script');
script.src = 'app.js';
script.async = true; // or false for defer-like behavior
document.head.appendChild(script);

By default, dynamically created scripts behave like async (they don’t block, but execute as soon as downloaded). Set script.async = false to make them behave like defer (ordered, but still execute after DOM).

Detecting Script Load

Use the load event on script elements:

script.onload = function() {
  console.log('Script loaded!');
};
script.onerror = function() {
  console.error('Failed to load script');
};
Demo: Dynamic Script Loading
HTML
<div id='dynamic-script-demo'>
<p><button id='load-script'>Load external script dynamically</button></p>
<pre id='script-log' style='background:#f1f5f9;padding:8px;border-radius:4px;'></pre>
</div>
JavaScript
const log = document.getElementById('script-log');

document.getElementById('load-script').onclick = function() {
const script = document.createElement('script');
script.src = 'data:text/javascript,' + encodeURIComponent(
'document.getElementById('script-log').textContent += 'Dynamic script executed!' + new Date().toLocaleTimeString() + '\n;'
);
script.async = true;

script.onload = function() {
log.textContent += 'Script load event fired\\n';
this.disabled = true;
this.textContent = 'Loaded';
};

script.onerror = function() {
log.textContent = 'Script failed to load';
};

document.head.appendChild(script);
log.textContent = 'Script element created and appended\\n';
};
Live Output Window

Module Scripts

For modern JavaScript (ES modules), the type="module" attribute behaves like defer by default:

<script type="module" src="app.js"></script>

Module scripts:

  • Are deferred by default (download in parallel, execute after parse)
  • Can use import and export
  • Strict mode by default
  • Execute only once (even if included multiple times)

Key Takeaways

  • Regular scripts block HTML parsing β€” avoid them in <head>
  • defer = download in parallel, execute in order after DOM ready
  • async = download in parallel, execute immediately when ready
  • Use defer for your code (needs DOM, depends on other scripts)
  • Use async for third-party scripts (analytics, widgets)
  • Dynamic scripts behave like async by default
  • type="module" behaves like defer automatically