Website Spec
← Performance
Recommended

Script loading — defer, async, module

Choose the right script-loading attribute for every <script>: defer for app code, async for independent third-party, type=module for modern code. Bare <script> in <head> is always wrong.

What it is

The <script> element has four loading modes that differ in when the browser pauses HTML parsing and when the script runs. Pick the wrong one and you block paint, break execution order, or ship code older browsers cannot evaluate.

<!-- 1. Classic blocking — never use in <head> -->
<script src="/app.js"></script>

<!-- 2. async — parallel download, runs as soon as ready, may run before DOMContentLoaded -->
<script src="https://cdn.example.com/analytics.js" async
        integrity="sha384-OqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>

<!-- 3. defer — parallel download, runs after parsing in document order -->
<script src="/app.js" defer></script>

<!-- 4. module — parallel download, deferred by default, ESM with imports -->
<script src="/app.js" type="module"></script>

Plus the platform extras: nomodule for legacy fallback, crossorigin for CORS, integrity for SRI, fetchpriority for prioritisation hints, blocking="render" for the rare case you genuinely need a blocking script.

Why it matters

A <script> in <head> with no async, defer, or type="module" halts HTML parsing while it downloads and executes. On a slow network, that one tag can delay the first paint by seconds. Done wrong site-wide, render-blocking scripts are the single biggest cause of poor LCP and the most common Lighthouse failure on real sites.

The four modes:

For first-party app code, the practical answer in 2026 is almost always type="module" or defer, placed in <head>.

How to implement

Default to <script defer src="…"> in <head> for app code. It starts downloading early (parallel with HTML), runs in order, and runs before DOMContentLoaded. Cleaner than placing scripts at the end of <body>, equally non-blocking, and works in every browser that matters.

<head>
  <script src="/app.js" defer></script>
  <script src="/widget.js" defer></script>
</head>

app.js runs before widget.js. Both run before DOMContentLoaded.

Use async for independent third-party. Analytics, A/B tooling, chat widgets — anything that doesn't depend on your code or DOM order. Always pair with Subresource Integrity and crossorigin="anonymous" so a compromised CDN cannot ship modified code to your visitors:

<script src="https://cdn.example.com/widget.js" async
        integrity="sha384-OqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous"></script>

Some vendors (rolling-release analytics, A/B platforms) ship a script whose contents change at the vendor's discretion and therefore cannot be pinned with SRI. In that case the integrity guarantee becomes "I trust this vendor with my origin" — make that an explicit risk decision, document it, and consider a strict CSP with a per-deploy allow-list as compensating control.

If two third-party scripts depend on each other, neither can be async. Use defer on both.

Use type="module" for modern code. It's defer by default, supports import, and gives you a single canonical place for tree-shaking, code-splitting, and dynamic import() for route-level lazy loading.

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

Preload module dependencies. Modules fetched via import are discovered lazily — preload them so the browser starts fetching during initial parse:

<link rel="modulepreload" href="/lib/router.js">
<link rel="modulepreload" href="/lib/store.js">
<script type="module" src="/app.js"></script>

See resource hints for the decision table.

Drop the legacy nomodule shim. Every shipping browser in 2026 supports type="module". The nomodule fallback was useful in 2018; it now ships a second bundle for users who don't exist.

Inline tiny critical scripts in <head> if and only if they must run before paint and are small enough that the cost of inlining is less than the cost of a separate request. Theme colour application, CSP setup, FOUC prevention — yes. A 50 KB framework runtime — no.

Combine with fetchpriority for the rare case you have a script that's deferred but critical:

<script src="/critical.js" defer fetchpriority="high"></script>

Set CSP allow-listing and SRI (subresource integrity) on every third-party script. The loading attribute does not change those obligations.

Common mistakes

Verification

Sources