Code of the Day
AdvancedBrowser Rendering

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.

Web FoundationsAdvanced7 min read
By the end of this lesson you will be able to:
  • 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 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

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.

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).

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;
}
ValueBehaviour
autoBrowser default (often FOIT)
blockInvisible text for up to 3 s, then swap
swapFallback immediately, swap when font loads (FOUT)
fallbackInvisible for 100 ms, then fallback; swap within 3 s
optional100 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.

Finished reading? Mark it complete to track your progress.

On this page