Skip to main content

Scripts in head

A script in <head> without a defer or async attribute stops HTML parsing: the browser has to download and execute the JS before it can carry on building the DOM. Because of that the user sees an empty page until the file arrives over the network, and every speed metric gets worse.

Theory

TL;DR

  • A synchronous <script src> blocks the HTML parser until the file has downloaded and run.
  • The browser does this because the script may change the document, for example through document.write.
  • In <head> this hurts most: the DOM is still empty, so the screen is white.
  • FCP, LCP, TTI and TBT all suffer, especially on a mobile network.
  • The cures: defer for the main code, async for independent scripts, or the tag at the end of <body>.
  • A tiny inline script in <head> is acceptable, as long as it really is tiny.

Quick example

html
<head> <script src="main.js"></script> </head> <body> <h1>Hello!</h1> </body>

Step by step:

  1. The browser starts reading the HTML.
  2. It reaches <script src="main.js">.
  3. It stops parsing the HTML.
  4. It waits for main.js to arrive over the network, which can take hundreds of milliseconds.
  5. It executes the script.
  6. And only then does it carry on parsing the rest of the page.

For that whole time the DOM has not been created, the user sees no content, and the FCP and LCP metrics suffer.

How the browser loads a page

When the browser receives an HTML document, it goes through these steps:

  1. It parses the HTML line by line, top to bottom.
  2. When it meets a <link> tag with CSS, it loads the stylesheet, because styles affect rendering.
  3. When it meets a <script> tag, it stops parsing in order to execute the JS.

Why? Because the script can change the DOM itself, or even remove elements that come later in the HTML (document.write, innerHTML and so on). So the browser is obliged to wait for the JS to finish before it continues parsing.

Schematically, the synchronous case looks like this:

text
HTML: <head> -> <script> -> <body> | download [======= JS is downloading =======] | execution [======= JS is executing =======] | only then is the DOM built

With defer the download happens in parallel and the execution is pushed to the end:

text
HTML and JS download together -> DOM is built -> JS executes

The problems this causes

ProblemWhat happens
The HTML parser is blockedThe page is not built until the JS has downloaded
A long "white page"The user sees emptiness
Slow networks, worse UXEspecially on 3G and 4G
Worse Core Web VitalsLCP, FCP, TTI, TBT all go up
Scripts get in the way of critical CSSCritical styles are applied too late

How to avoid the blocking

Option 1. defer

html
<script src="main.js" defer></script>

The script downloads in parallel with the HTML but executes only after the DOM is fully parsed, just before the DOMContentLoaded event. No blocking, a guaranteed execution order, and the ideal choice for most cases.

Option 2. async

html
<script src="analytics.js" async></script>

The script downloads asynchronously in the background and executes as soon as it has downloaded, without waiting for anything else. It is used for independent scripts: analytics, advertising, tracking pixels. The execution order is not guaranteed.

Option 3. Move the scripts to the end of <body>

html
<body> ... <script src="main.js"></script> </body>

By the time the JS loads, the HTML is already fully parsed, the user can see the content, and the JS runs as the final step. This is the old but still working approach; in modern HTML <script defer> has replaced it.

The effect on metrics

MetricWhat degrades with scripts in <head>
FCP (First Contentful Paint)The first content takes longer to appear
LCP (Largest Contentful Paint)The main element renders later
TTI (Time to Interactive)JS blocks the main thread
TBT (Total Blocking Time)Grows because of heavy synchronous scripts

What to do in production

Script typeWhere and how to include it
Main logic (UI, SPA)<script src="main.js" defer>
Analytics, advertising<script src="metrics.js" async>
Inline initialisation<script> inside <head>, if the initialisation really is small
Legacy librariesBetter moved to the end of <body>

Summary table:

ScenarioWhat it doesConsequence
<script> in <head> with no attributesBlocks parsingSlow
<script defer>Downloads in parallel, executes after the DOMOptimal
<script async>Downloads and executes independentlyOnly for independent scripts
<script> at the bottom of <body>Waits for the end of the HTMLNo blocking

Common mistakes

  • Thinking the problem is <head> itself. What blocks is synchronous execution, not the position: <script defer> in <head> is the best option there is.
  • Putting async on code that depends on the DOM or on another file. It can run before the elements exist and break at random.
  • Hiding the problem behind window.onload. The script still downloads synchronously and delays parsing, the logic inside simply waits longer.
  • Forgetting about render-blocking CSS. A heavy <link rel="stylesheet"> also delays rendering, so critical styles are worth inlining.
  • Putting a huge inline script in <head>. The network is irrelevant here, but parsing and execution still occupy the main thread.
  • Including third-party widgets without async. A slow foreign server then directly delays the rendering of your own page.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.