skip to content

In an HTML page, what is the difference between a parser-blocking resource and a render-blocking one, and why does a synchronous <script> placed after a <link rel="stylesheet"> wait for that stylesheet before it runs?

level: middleimportance: must knowfreq 48%

answer

  1. parse versus paint are different blocks
  2. scripts can read computed style
  3. CSSOM must be settled first
  4. download proceeds, execution is held
  5. defer keeps the parser moving

basics

~20 s

Parser-blocking stops the HTML parser from building more DOM; render-blocking only withholds the first paint while parsing continues. A synchronous script waits for preceding stylesheets because it may read computed styles, which requires a complete CSSOM.

solid answer

~50 s

Render-blocking means the browser will not paint until the resource is processed, but the HTML parser keeps building DOM in the meantime — that is a plain stylesheet. Parser-blocking is stronger: the parser stops dead at the tag until the resource downloads and executes, so no further DOM is built at all — that is a synchronous `<script>`. The two combine in a way that surprises people: a script may call `getComputedStyle()` or read `offsetWidth`, and those answers are only correct once the CSSOM is complete. So the browser holds the script's *execution* until every stylesheet that precedes it has loaded and parsed. The stylesheet is still render-blocking by nature, but through the script it now blocks parsing too — which is why a slow CSS file plus a synchronous script in the head is the classic worst case.

code

html · 21 lines
html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Blocking demo</title>

  <!-- runs immediately: it precedes the stylesheet -->
  <script>window.tStart = performance.now();</script>

  <link rel="stylesheet" href="/app.css">

  <!-- downloaded in parallel, but EXECUTION waits for app.css -->
  <script>
    console.log('held for CSS:', performance.now() - window.tStart, 'ms');
    console.log('width:', document.documentElement.offsetWidth);
  </script>
</head>
<body>
  <p>Content</p>
</body>
</html>

go deeper

for a junior

Know that a plain script tag halts HTML parsing where it sits while a stylesheet does not, and that adding defer is the everyday fix for a script in the head.

for a middle

Explain the interaction precisely: a pending stylesheet delays the execution of a following synchronous script because that script may read computed style, so the CSS becomes a parser stall too.

for a senior

Show you can spot this from a real trace — a main thread idle behind a script that is idle behind CSS — and prescribe ordering and defer changes rather than blanket "move scripts to the bottom" advice.

for a principal

Own the rule that governs what may enter the head at all, since a single synchronous third-party tag imports someone else's latency into your parse step on every page of the site.

## Two different things a resource can block The words are used loosely in blog posts, but they describe genuinely different behaviours and interviewers use the distinction to see whether you have a mechanism or a slogan. **Render-blocking** — the browser will not produce a first paint until this resource has been fetched and processed. It says nothing about parsing. While an external stylesheet is in flight, the HTML parser happily continues, builds more DOM, and a preload scanner reads ahead through the raw markup and starts downloading whatever else it finds. Nothing is drawn, but a great deal of useful work happens. **Parser-blocking** — the browser stops constructing the DOM at the position of this tag until the resource is fetched and run. A classic `<script src="...">` with neither `defer` nor `async` does this, and so does an inline `<script>` (there is nothing to fetch, but it still executes synchronously at that point in the document). The reason is historical and unavoidable: the script may call `document.write()` and inject markup at that exact position, so the parser cannot legally run ahead of it. Parser-blocking implies render-blocking as a consequence — if there is no more DOM, there is no more content to paint — but the reverse is not true. ## Why CSS ends up blocking a script This is the part of the question that separates answers. Consider: ```html <head> <link rel="stylesheet" href="/app.css"> <script> // may run only after app.css has loaded and parsed const wide = document.documentElement.offsetWidth > 900; </script> </head> ``` JavaScript can interrogate style: `getComputedStyle(el)`, `el.offsetWidth`, `el.getBoundingClientRect()`. Those APIs must return the values that reflect *all* the CSS the document has referenced so far. If the browser let the script run while `app.css` was still downloading, the script would read pre-stylesheet values, and reloading the page on a faster connection would give a different answer — a race condition baked into the platform. So the HTML specification defines the notion of a style sheet that is **blocking scripts**: a pending stylesheet delays the execution of any script that comes after it in the document. The download of the script still proceeds in parallel — only execution is held. And because that script is itself parser-blocking, the parser is now stalled behind a *CSS* download. The stylesheet has, transitively, become parser-blocking. The practical shape of the worst case: 1. The parser reaches `<link rel="stylesheet">` and starts the CSS download. DOM building continues. 2. It reaches a synchronous `<script>` and stops building DOM. 3. The script downloads quickly but cannot execute — the CSS has not arrived. 4. Only when CSS lands does the script run, the parser resume, and eventually the page paint. Two serialized waits where a naive reading of the markup suggests one. ## What actually changes the outcome - **Do not put synchronous scripts in the head.** `defer` keeps the script out of the parser's way entirely and runs it after parsing, in document order. `async` runs it as soon as it arrives, order unspecified. Neither is parser-blocking. A module script (`type="module"`) is deferred by default. - **Do not let a script that must be synchronous sit behind a large stylesheet.** If a tiny inline script must run early — an anti-flicker or feature-detection snippet — put it *before* the stylesheet link, and it will not be held by CSS at all. Ordering is a real lever here. - **Shrink the CSS on the path**, because you are now paying for it twice: once for paint, once for script execution. ## The nuances worth knowing A stylesheet loaded with a `media` attribute that does not match the current environment is not render-blocking, and does not block scripts either — the browser still downloads it, at low priority, but does not wait on it. That is the basis of several deferral tricks. An `async` or `defer` script is never held by a pending stylesheet in the parser-blocking sense, though a `defer` script naturally runs after parsing has completed anyway. And a subtle one that trips people up in debugging: because the preload scanner starts downloads ahead of the stalled parser, the *network* panel may show everything downloading in parallel and look perfectly healthy, while the *main thread* is idle waiting. You need the main-thread timeline, not just the waterfall, to see a parser stall. ## How to answer it out loud Name both terms, give the one-line difference (parse versus paint), then explain the interaction: scripts can read style, so style must be settled before they run, so CSS transitively blocks parsing whenever a synchronous script follows it. Finish with the fix — `defer` by default, and order the rare must-run-early inline snippet before the stylesheet.

  • If a small inline script must run before anything else, where do you put it relative to the stylesheet link?
    Before it. A pending stylesheet only blocks scripts that come *after* it in document order, so an inline snippet placed above the `<link>` executes immediately with no CSS wait. Put it below and it inherits the full stylesheet download as a parser stall, which is the usual cause of a mysteriously slow head.
  • Does a stylesheet with a non-matching media attribute still block scripts that follow it?
    No. A stylesheet the browser knows does not apply to the current environment is not render-blocking and does not block script execution. It is still downloaded, at a lower priority, because the environment can change — a resize or a print action can make it match later.
  • The network waterfall shows everything downloading in parallel, yet first paint is late. What is happening?
    The preload scanner is doing its job — it reads ahead in the raw HTML and starts downloads while the parser is stalled. Parallel downloads therefore prove nothing about parsing. Look at the main-thread timeline instead: you will typically find a gap where the parser is waiting on a script that is itself waiting on CSS.

saying these in an interview costs you the question

  • Says a stylesheet stops the HTML parser outright
  • Claims the script's download, not just its execution, is delayed
  • Thinks defer and async scripts also wait for pending CSS
  • States that scripts cannot read style until after load anyway
  • Assumes parallel downloads in the waterfall mean nothing is blocked

context