Loading Strategies
The browser loads resources in the order it encounters them — but defer, async, preload, and prefetch let you change that order to serve users faster.
- Explain the default script-loading behaviour and why it blocks parsing
- Distinguish defer from async and choose the right one for a given use case
- Use preload, prefetch, and preconnect to prioritise resources before they are discovered
- Apply font-display to prevent invisible text during font load
You cannot make the network faster, but you can change when the browser starts each request and what it does while it waits. The attributes and hints in this lesson let you express your intent — "this script is not urgent", "that image is critical", "I know the user will navigate to this page next" — and the browser acts accordingly.
Default script loading (blocking)
Without any attributes, a <script> tag is a full stop in HTML parsing:
<!-- parser reaches this line and stops -->
<script src="heavy-library.js"></script>
<!-- parsing resumes only after the script downloads AND executes -->The browser halts the HTML parser, waits for the file to download from the network, executes the JavaScript, and only then continues building the DOM. On a slow connection this can add seconds to the time before any content appears.
defer — the recommended default
defer tells the browser to download the script in parallel with HTML parsing,
but execute it only after the DOM is fully built, in the order the scripts
appear in the document:
<head>
<script defer src="app.js"></script>
<script defer src="analytics.js"></script>
</head>Both scripts download in parallel with HTML parsing. When parsing is complete,
app.js runs first, then analytics.js — order is preserved. The DOM is
fully constructed before either script runs, so document.querySelector and
similar calls work without any DOMContentLoaded wrapper.
Use defer for almost every script that is not needed before the DOM is
available. It is the safe, predictable default.
async — for independent scripts
async also downloads in parallel with HTML parsing, but executes the script
the moment it finishes downloading — interrupting HTML parsing at that point,
regardless of document order:
<head>
<script async src="analytics.js"></script>
<script async src="chat-widget.js"></script>
</head>Whichever script finishes downloading first runs first. Document order is not
respected. This makes async appropriate only for scripts that:
- Have no dependencies on other scripts
- Do not depend on the DOM being fully built
- Do not need to run in a predictable order
Third-party analytics tags and advertising scripts are the canonical use case.
|--- HTML parsing ---|
default: |---download---|---execute---|
defer: |---download---| |---execute (in order)---|
async: |---download---| |--exec--|Do not use async for scripts that call document.write() or that depend on
another script having run first. The unpredictable execution order breaks
those assumptions silently.
type="module" — always deferred
Scripts with type="module" are deferred automatically — you get defer
behaviour for free, plus ES module import/export syntax:
<script type="module" src="main.js"></script>Modules also run in strict mode by default, have their own scope (no accidental
globals), and can use top-level await. If you are writing modern JavaScript,
type="module" is the right choice and defer is redundant.
Resource hints
<link rel="preload">
The browser's preload scanner reads ahead in the HTML to discover resources
early. But some resources are not visible until late — a font referenced deep
inside a CSS file, a hero image in the <body>, a critical JS chunk loaded
dynamically. preload brings those requests forward:
<link rel="preload" href="/fonts/brand.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/images/hero.jpg" as="image">
<link rel="preload" href="/js/app-chunk.js" as="script">The as attribute is required. It tells the browser the resource type so it
can set the right priority and apply correct headers (fonts need crossorigin
even for same-origin files, because they are fetched with CORS).
Preload is a high-priority hint — the browser starts the fetch immediately, alongside the HTML download. Use it only for resources that are definitely needed on the current page.
<link rel="prefetch">
Prefetch is a low-priority hint for resources likely needed on the next page a user will visit:
<link rel="prefetch" href="/next-page.html">
<link rel="prefetch" href="/images/product-detail.jpg">The browser fetches these in idle time, after the current page has loaded. If the user does navigate there, the resource is already in cache. If they do not, bandwidth is wasted — so use prefetch only when you have a high-confidence prediction of the next navigation (e.g., a checkout flow, a multi-step form).
<link rel="preconnect">
Opening a connection to a third-party origin involves DNS lookup, TCP
handshake, and TLS negotiation — 100–500 ms of overhead that happens before
the first byte of any resource from that origin. preconnect triggers that
setup early:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.example.com">Do not preconnect to more than two or three origins. Each open connection consumes resources on both the client and the server; unused connections are wasteful.
Web font loading and font-display
Custom web fonts add a download step before text can be rendered. The default browser behaviour during that download varies: some browsers hide text entirely (FOIT — flash of invisible text) while the font loads; others show a fallback font temporarily (FOUT — flash of unstyled text).
The CSS font-display property gives you control:
@font-face {
font-family: 'BrandFont';
src: url('/fonts/brand.woff2') format('woff2');
font-display: swap;
}| Value | Behaviour |
|---|---|
auto | Browser default (often FOIT) |
block | Invisible text for up to 3 s, then swap |
swap | Fallback immediately, swap when font loads (FOUT) |
fallback | Invisible for 100 ms, then fallback; swap within 3 s |
optional | 100 ms block; if not loaded, uses fallback for this load |
font-display: swap is the most common recommendation: users see text
immediately in a system font, and the custom font swaps in once downloaded.
Pair it with a preload hint for the font file to minimise the swap duration:
<link rel="preload" href="/fonts/brand.woff2" as="font"
type="font/woff2" crossorigin>font-display: optional is worth considering for decorative fonts: if the
font does not arrive within the 100 ms block window, the browser skips it
entirely for this page load. No layout shift, no swap — at the cost of
inconsistent rendering across page loads.
Where to go next
You now have the full toolkit for controlling the critical rendering path. The lab — Lab — Profile with DevTools — puts everything into practice: you will profile a deliberately slow page, identify the bottlenecks using the Network and Performance tabs, and apply these strategies to measure the improvement.
The Critical Rendering Path
The critical rendering path is the sequence of steps the browser must complete before painting the first visible frame — optimizing it is the single biggest lever on perceived load speed.
Lab — Profile with DevTools
Use Chrome DevTools to measure the rendering pipeline of a deliberately slow page, identify the bottlenecks, and apply targeted fixes.