Modern HTML5 in Production · Loading Performance
Render-Blocking Scripts
Why a blank white page showed for two seconds before anything appeared, and how defer, async and modules let HTML render while scripts download.
Scenario
Example app: a site's <head> contains five <script src> tags: a framework bundle, an analytics library, a chat widget, an A/B testing tool and the app code. The content is server-rendered HTML.
What you see in production
- Users see a blank page for almost three seconds on mobile, even though the HTML arrives in 300 ms.
- First Contentful Paint and LCP are poor; the chat widget's slow CDN sometimes delays the whole page.
The naive code
<head>
<script src="/vendor/framework.js"></script>
<script src="https://analytics.example.com/a.js"></script>
<script src="https://chat.example.com/widget.js"></script>
<script src="https://ab.example.com/test.js"></script>
<script src="/app.js"></script>
</head>
Reproduce it
A classic script stops the HTML parser until it has downloaded and run. The model compares when content can first appear.
const htmlMs = 300;
const scripts = [
{ name: "framework.js", download: 400, run: 120 },
{ name: "analytics.js", download: 350, run: 40 },
{ name: "chat-widget.js", download: 700, run: 90 },
{ name: "ab-test.js", download: 300, run: 60 },
{ name: "app.js", download: 250, run: 150 },
];
// classic scripts in <head>: each blocks parsing until downloaded and run, one after another
let blockingRender = htmlMs;
for (const s of scripts) blockingRender += s.download + s.run;
// defer: download in parallel while parsing; content renders after parsing; scripts run afterwards in order
const deferRender = htmlMs + 50; // parse the rest of the HTML and paint
const deferInteractive = Math.max(htmlMs, Math.max(...scripts.map((s) => s.download))) + scripts.reduce((t, s) => t + s.run, 0);
console.log(`classic scripts in head: first paint ~${blockingRender} ms`);
console.log(`defer (and async for analytics): first paint ~${deferRender} ms, all scripts done ~${deferInteractive} ms`);
Output
classic scripts in head: first paint ~2760 ms defer (and async for analytics): first paint ~350 ms, all scripts done ~1160 ms
Root cause
Classic <script> tags are parser-blocking: the browser must download and execute them before it continues building the page, because scripts might change the document. Third-party scripts also add DNS, TLS and download time from other servers, and a slow one blocks everything after it.
The fix
From MDN's script element reference:
deferfor application code: downloads in parallel, runs after parsing, in order.asyncfor independent scripts (analytics) that don't depend on others or on the DOM being complete.type="module"scripts are deferred by default.- Load non-essential widgets (chat, surveys) later: on interaction or after the page is idle.
- Keep A/B testing that must change the first render on the server side, or accept its cost knowingly.
- Use
<link rel="preconnect">for unavoidable third-party origins.
Trade-offs
asyncscripts run in any order; don't use it for scripts that depend on each other.- Deferring everything can delay interactivity for code-driven UIs; split bundles so critical code is small.
Detect it in production
- Lighthouse's "Eliminate render-blocking resources" audit and the Performance panel's main-thread timeline.
- Monitor third-party script timing and set budgets for third-party bytes.
Key points
- Classic scripts block parsing and rendering.
- Use
deferor modules for app code,asyncfor independent scripts, and load widgets late. - Measure first paint and third-party cost in real-user monitoring.
Practice questions
What is the difference between async and defer on a script tag?
Answer
Both download in parallel with parsing. defer scripts run after the HTML is parsed, in document order; async scripts run as soon as each one arrives, in any order. Module scripts are deferred by default.
Why does a script in the head without defer delay rendering?
Answer
The parser must stop to download and execute it, because it might modify the document, so no content after it can be built or painted until it finishes.
How should third-party widgets such as chat be loaded?
Answer
Asynchronously and late (after the page is interactive, on idle, or on first interaction), with preconnect where useful, so they can't block or slow the first render.
Open in the app to mark lessons complete, take notes and track your progress.