How browsers render contentWhat blocks the parser and the first paint

100%

What blocks the parser and the first paint.

A browser that has your whole HTML file can still show a white screen. It is not slow at drawing; it is waiting, on purpose. It will not paint until it has the stylesheets it was told about, it stops reading the page whenever a plain script tag turns up, and it may draw your headline in invisible ink until a font arrives. Each of those waits protects something, and each has an attribute or a habit that turns it off when it is not needed. Knowing which is which is the difference between a page that appears at half a second and the same page appearing at well over one.

Beginner34 minUpdated 2 Oct 2026

Builds on From bytes to pixels.

The idea.

The HTML arriving is not the page appearing. In between, the browser waits for some resources on purpose, and the order of a few tags in your head decides how long.

Copperpot, the kitchenware shop we follow through this card, has a product page for an enamel casserole. Open it on a mid-range phone over 4G and the server has sent every byte of HTML by 0.42 s. The screen stays white until 1.22 s, and even then the name, the price and the details are missing for another 0.3 s: the boxes are laid out, the bullets are there, the words are not. Nothing was slow to draw. The browser spent most of that second waiting, because the page told it to.

Each wait exists for a good reason. Painting before the stylesheet arrives would show raw, unstyled HTML and then jump, so the browser holds the first paint until the stylesheets it has seen in the head are ready: they are render-blocking. A plain script tag may call document.write or read a style and expect the page as it stands at that line, so the parser stops, fetches the script, waits for any pending stylesheet the script might read, runs it, and only then reads on: the script is parser-blocking. A heading set in a web font could flash from one typeface to another, so by default the browser draws that text invisibly for up to a few seconds while the font downloads.

Most pages do not need all of those protections for all of their resources. A print stylesheet cannot affect the screen. An analytics script does not care where the parser is. The app's own bundle only needs the finished DOM. Attributes such as media, async and defer tell the browser which waits it may skip, and a second, speculative parser starts downloads early even while the main one is stuck. This topic is about who waits for whom, and how to change it.

The same page at four moments

Every byte of HTML is here, yet nothing is drawn. The parser is stuck on the consent script in the head, which is still connecting to its own origin, and the document has no body yet, so it cannot render.

0.42 s, HTML complete. Document page: 0 blocks. Status: “Blank: waiting on the head” (busy).

Who waits for whom, by default

In the HTMLHolds back the first paintStops the parserHolds back scripts
<link rel=stylesheet> in headYes, until it loadsNoYes: plain and deferred scripts wait for it
<link rel=stylesheet media=print>No (media does not match)NoNo
<script src> (no attributes)In the head, yes: no body exists while the parser is stoppedYes, while it downloads and runs-
<script> inlineIn the head, yesYes, while it runs (and while it waits for pending CSS)-
<script async src>NoOnly while it runs-
<script defer src>, <script type=module>NoNo-
<img src>NoNoNo
@font-face fontNo, but text using it is drawn invisibly for a whileNoNo
Was this section helpful?

How it works.

Six parts take turns on Copperpot's page: the parser, a second parser that reads ahead, the network, the style engine, the script engine and the renderer. Who is allowed to go next depends on a handful of rules.

Copperpot's head, as it was, from first byte to readable text

Copperpot's head, as it was, from first byte to readable text, as an ordered list of steps:
Copperpot's head, as it was, from first byte to readable text16 steps between HTML parser, Preload scanner, Network service, Style engine, Script engine, Renderer. The steps are listed as text after the diagram.RendererScript engineStyle engineNetwork servicePreload scannerHTML parserplain script: tokenizer stops hereresumes; body parsed by 1,160 msfirst paint 1,220 ms, text invisiblefirst HTML chunk (300 ms)1GET main.css (matches screen: render-blocking)2GET print.css (media=print: low priority, nobody waits)3GET consent.js from a new origin4read ahead: GET vendor.js, product.js, hero photo, reviews.js5main.css (700 ms): CSSOM built6consent.js (760 ms)7run consent.js, then the inline snippet8vendor.js (900 ms)9run vendor.js, then product.js (to 1,110 ms)10body exists and nothing is render-blocking11GET Fraunces (style finds text that uses it)12font (1,500 ms): text drawn13
  1. Network service → HTML parser (reply): first HTML chunk (300 ms)
  2. HTML parser → Network service: GET main.css (matches screen: render-blocking)
  3. HTML parser → Network service: GET print.css (media=print: low priority, nobody waits)
  4. HTML parser → Network service: GET consent.js from a new origin
  5. Note over HTML parser: plain script: tokenizer stops here
  6. Preload scanner → Network service: read ahead: GET vendor.js, product.js, hero photo, reviews.js
  7. Network service → Style engine (reply): main.css (700 ms): CSSOM built
  8. Network service → HTML parser (reply): consent.js (760 ms)
  9. HTML parser → Script engine: run consent.js, then the inline snippet
  10. Network service → HTML parser (reply): vendor.js (900 ms)
  11. HTML parser → Script engine: run vendor.js, then product.js (to 1,110 ms)
  12. Note over HTML parser: resumes; body parsed by 1,160 ms
  13. HTML parser → Renderer: body exists and nothing is render-blocking
  14. Renderer → Network service: GET Fraunces (style finds text that uses it)
  15. Note over Renderer: first paint 1,220 ms, text invisible
  16. Network service → Renderer (reply): font (1,500 ms): text drawn

Stylesheets hold back the paint

The HTML Standard keeps a list on every document: the elements it is waiting for before it may render. A stylesheet link or a style element that the parser creates joins that list if its media attribute matches the current screen, and leaves it once the sheet and the resources it needs to be applied have loaded. While the list has anything in it, or while the document has no body element yet, the document is render-blocked: rendering opportunities come and go and the browser skips them. Browsers also keep a safety timeout of their own choosing, so a stylesheet that never finishes cannot hold the page back for ever.

Two details follow from that wording. First, elements can only join the list while there is no body yet, so it is the head's stylesheets that gate the first paint. Second, the media check is made against the environment right now: media=print, or a min-width the phone does not reach, means the sheet is still downloaded, at a low priority, but it blocks nothing.

The same matching, parser-created stylesheets also block scripts. Any plain or deferred script waits until no such sheet is still loading, because the script might read a computed style or a box size, and an answer based on half the CSS would be wrong. That is why a tiny inline script placed after a stylesheet link is not free: it cannot run, and the parser cannot move past it, until the stylesheet has arrived. Moving the inline snippet above the link, when it does not read styles, removes that wait.

What each link costs the first paint

<!-- waits: matches the screen, so it blocks rendering and later scripts -->
<link rel="stylesheet" href="/css/main.css">

<!-- fetched at low priority, blocks nothing on screen -->
<link rel="stylesheet" href="/css/print.css" media="print">

<!-- blocks only on wide screens; on Copperpot's 393 px phone it does not -->
<link rel="stylesheet" href="/css/wide.css" media="(min-width: 64em)">

<!-- this inline script cannot run until main.css has loaded -->
<script>window.cpLayer = [{ page: "product", sku: "CAS-24-TEAL" }];</script>

Plain scripts stop the parser

A script tag with a src and no async or defer is parser-blocking. When the parser reaches it, it stops building the DOM, waits for the file to download, waits for any stylesheet that blocks scripts, runs the script, and only then reads the next tag. An inline script skips the download but not the rest. In the head this also means no body exists yet, so nothing can paint while the parser is stopped there.

Two attributes change the deal for external scripts. async says: download alongside the parser and run the moment you arrive, whenever that is. The parser is only paused while the script actually runs, but the order between async scripts is whatever order the downloads finish in, and an async script does not wait for stylesheets. defer says: download alongside the parser, then run after parsing has finished, in the order the tags appear, before DOMContentLoaded. A deferred script does wait for script-blocking stylesheets, so it always sees the page's styles.

Module scripts (type=module) are deferred without being asked, and the defer attribute does nothing on them; add async to make a module run as soon as it and its imports are ready. Inline classic scripts ignore both attributes. Scripts that other scripts insert with createElement are async by default; setting script.async = false on them makes a group of injected scripts run in the order they were inserted, which is how loaders keep dependencies in line without blocking the parser.

Two scripts, three ways to load them

Scenario 1 of 3: As described.

Timeline as a list

Two scripts, three ways to load them: 3 lanes, from 0 ms to 600 ms.

  1. 0–50 ms · HTML parser · parse
  2. 50–400 ms · HTML parser · stopped at a.js
  3. 50–350 ms · a.js (300 ms download) · download
  4. 60–160 ms · b.js (100 ms download) · early fetch
  5. 350–400 ms · a.js (300 ms download) · run
  6. 400–420 ms · HTML parser ·
  7. 420–440 ms · HTML parser · b.js
  8. 420–440 ms · b.js (100 ms download) · run
  9. 440–570 ms · HTML parser · parse
  10. 570 ms · HTML parser · DOMContentLoaded (ok)
A page whose head holds a.js (big, 300 ms to download) and then b.js (small, 100 ms), with 200 ms of HTML to parse in all. Switch tabs to see the same two files as plain, async and deferred scripts. Times are illustrative.

Script loading at a glance

TagDownloadRunsOrder keptWaits for stylesheetsDOMContentLoaded waits
<script src>Parser stops for itAt its place in the HTMLYesYesYes
<script> (inline)NoneAt its place in the HTMLYesYesYes
<script async src>In parallelAs soon as it arrivesNoNoNo
<script defer src>In parallelAfter parsing endsYes, document orderYesYes
<script type=module>In parallel, with its importsAfter parsing endsYes, document orderYesYes
<script type=module async>In parallel, with its importsAs soon as the graph is readyNoNoNo
injected with createElementIn parallelAs soon as it arrives (async by default)Only with script.async = falseNoNo

A document's readiness, from first byte to load

States of1HTML parser

A document's readiness, from first byte to load. 5 states, 6 transitions. The table below lists them.
A document's readiness, from first byte to loadThe states of HTML parser. 5 states, 6 transitions. The table below lists them.

‹script› without async/defer

fetched and run [no CSS blocking scripts]

async script arrives / pause while it runs

last byte parsed / readyState = interactive

deferred and module scripts ran / fire DOMContentLoaded

nothing delays the load event / fire load

Parsing (loading)

Stopped at a plain script

Running deferred scripts (interactive)

DOMContentLoaded fired

Loaded (complete)

7 steps. Three plain scripts in the head, no deferred ones.

The states the parser moves the document through. readyState only has three values (loading, interactive, complete); the machine splits them where the rules for scripts change. DOMContentLoaded is the arrow out of the deferred scripts, not the end of parsing.

Transitions of A document's readiness, from first byte to load
From → ToEventGuardAction
Parsing (loading) → Stopped at a plain script<script> without async/defer
Stopped at a plain script → Parsing (loading)fetched and runno CSS blocking scripts
Parsing (loading) → Parsing (loading)async script arrivespause while it runs
Parsing (loading) → Running deferred scripts (interactive)last byte parsedreadyState = interactive
Running deferred scripts (interactive) → DOMContentLoaded fireddeferred and module scripts ranfire DOMContentLoaded
DOMContentLoaded fired → Loaded (complete)nothing delays the load eventfire load
Parsing (loading)start
readyState is loading; plain scripts run in place
Stopped at a plain script
Waiting for its download and for script-blocking CSS
Running deferred scripts (interactive)
readyState is already interactive
DOMContentLoaded fired
Images, async scripts and frames may still be loading
Loaded (complete)end
readyState complete, then the load event

The second parser that reads ahead

If a stopped parser were the whole story, every file after a plain script would start downloading only once that script had run, and a head with three scripts would cost three round trips in a row. Browsers avoid that with a speculative parser, usually called the preload scanner. While the real parser is stuck, it reads on through the raw HTML that has already arrived, recognises URLs in tags it knows (img src and srcset, stylesheet links, script src, preload links) and starts those downloads. It builds nothing and runs nothing; it only gets the bytes moving.

That is why vendor.js, product.js and Copperpot's hero photo were all requested at about 320 ms in the sequence above, even though the parser did not reach them until much later. The scanner can only see what is written in the HTML, though. Anything that a script adds, anything named only inside CSS, and anything hidden in a data attribute for a lazy loader is invisible to it, and waits for the stage that does see it.

What the preload scanner can start early

Seen in the HTML
<img src> and srcsetthe hero photo, if it is a real img tag
<link rel=stylesheet>
<script src>, with or without async and defer
<link rel=preload> and other hintstheir rules belong to Resource hints, priorities and fonts
Invisible to it
Scripts and images that JavaScript insertsrequested only when that script has run
CSS background images and @font-face fontsfound by the style engine, or later, at layout
Images whose URL sits in data-src for a lazy loader
Markup rendered on the client from JSONthe resources inside it are found after hydration or render

Fonts do not block the paint, they hide the text

A web font is not render-blocking in the stylesheet sense. Its @font-face rule only says where the file is; the browser downloads it once styles show that text on the page actually uses it, which means after the CSS has been parsed and usually around the first layout. That makes fonts late by design, and the preload scanner cannot help, because the URL lives in CSS.

What happens to the text meanwhile is set by a timeline in CSS Fonts Level 4. During the block period, text that wants the font is laid out and drawn with an invisible fallback, so the space is there but the words are not. During the swap period it is drawn in a visible fallback and swapped when the font lands. After that comes the failure period: the fallback stays. Chromium-based browsers and Firefox use a block period of up to 3 s by default (font-display: auto behaves like block there). The font-display descriptor sets the two periods: block keeps that short block (about 3 s) and an unlimited swap period; swap has almost no block period and an unlimited swap period; fallback a block of about 100 ms and a swap of about 3 s; optional the tiny block and no swap period at all, so a font that misses it is not used on this page view. Which one to choose, and how to get the file earlier, are covered in Resource hints, priorities and fonts.

The same heading with the default and with swap

Scenario 1 of 2: As described.

Timeline as a list

The same heading with the default and with swap: 3 lanes, from 0 ms to 4,000 ms.

  1. 0–3,400 ms · Fraunces download · downloading
  2. 0–3,000 ms · Text with the default policy · invisible (block period)
  3. 0–3,400 ms · Text with font-display: swap · fallback font
  4. 3,000–3,400 ms · Text with the default policy · fallback
  5. 3,000 ms · Text with the default policy · block period ends (deadline)
  6. 3,400–4,000 ms · Text with the default policy · Fraunces
  7. 3,400–4,000 ms · Text with font-display: swap · Fraunces
  8. 3,400 ms · Text with font-display: swap · swap (lines may move)
Time from the moment layout finds text that needs Fraunces. The first tab is a slow connection where the font takes 3.4 s; the second a quick one. The default policy behaves like block in Chromium and Firefox.

Asking for a block on purpose

Sometimes the default is too lenient. A deferred or module script that sets a theme class, or picks the variant of an experiment, would cause a visible flash if the page painted before it ran. The blocking attribute with the value render, on a script, link or style element in the head, puts that element on the render-blocking list without making it parser-blocking. The parser keeps going; only the paint waits. It is supported in Chrome and Edge 105 and later and Safari 18.2 and later, and not in Firefox as of September 2026, where the page simply paints without waiting, so it must stay an improvement rather than a requirement.

Holding the paint, not the parser

<head>
  <link rel="stylesheet" href="/css/main.css">
  <!-- downloads in parallel and runs after parsing,
       but no frame is painted until it has run -->
  <script type="module" src="/js/theme.js" blocking="render"></script>
</head>
Was this section helpful?

In practice.

Copperpot's head, rewritten tag by tag: what each change bought, where the first 1.22 s went, how to find blockers on a real page and how to talk about them in an interview.

Copperpot's head, before and after

<head>
  <meta charset="utf-8">
  <title>Enamel casserole, 24 cm · Copperpot</title>
  <link rel="stylesheet" href="/css/main.css">
  <link rel="stylesheet" href="/css/print.css" media="print">
  <script src="https://consent.vendor.example/consent.js"></script>
  <script>window.cpLayer = [{ page: "product" }];</script>
  <script src="/js/vendor.js"></script>
  <script src="/js/product.js"></script>
</head>
<body>
  <img src="/img/casserole-teal-1200.avif" alt="…" width="1200" height="960">
  …
  <script async src="/js/reviews.js"></script>
</body>
<head>
  <meta charset="utf-8">
  <title>Enamel casserole, 24 cm · Copperpot</title>
  <!-- reads no styles, so it goes before the stylesheet -->
  <script>window.cpLayer = [{ page: "product" }];</script>
  <link rel="preload" href="/fonts/fraunces.woff2" as="font"
        type="font/woff2" crossorigin>
  <link rel="stylesheet" href="/css/main.css">  <!-- font-display: swap -->
  <link rel="stylesheet" href="/css/print.css" media="print">
  <script async src="https://consent.vendor.example/consent.js"></script>
  <script defer src="/js/vendor.js"></script>
  <script defer src="/js/product.js"></script>
</head>

Copperpot's first load, before and after

Scenario 1 of 2: As described.

Timeline as a list

Copperpot's first load, before and after: 8 lanes, from 0 ms to 1,600 ms.

  1. 0–300 ms · HTML · server
  2. 300–420 ms · HTML · HTML
  3. 300–315 ms · Main thread ·
  4. 310–700 ms · main.css (48 KB) · download
  5. 315–615 ms · consent.js (new origin) · DNS + TCP + TLS
  6. 315–760 ms · Main thread · parser stopped at consent.js
  7. 320–900 ms · vendor.js · download (scanner)
  8. 320–920 ms · product.js · download (scanner)
  9. 330–1,100 ms · Hero photo (140 KB) · download
  10. 615–760 ms · consent.js (new origin) · download
  11. 760–812 ms · Main thread · consent
  12. 812–900 ms · Main thread ·
  13. 900–1,060 ms · Main thread · vendor.js
  14. 1,060–1,110 ms · Main thread · product
  15. 1,110–1,160 ms · Main thread · body
  16. 1,160–1,200 ms · Main thread · reviews
  17. 1,162 ms · Main thread · DOMContentLoaded
  18. 1,200–1,500 ms · Fraunces font · download
  19. 1,200–1,220 ms · Main thread ·
  20. 1,220 ms · all lanes · first paint (deadline)
  21. 1,510 ms · Fraunces font · text visible (ok)
Assumed timings on a mid-range phone over 4G. The main-thread lane shows what the page is doing; the lanes above it show downloads. Before: the parser is stuck for most of the first second. After: only main.css stands between the HTML and the first paint.

Where the 1,220 ms before the first paint went

Before
  1. TTFB, width 300, Server (time to first byte), value 300
  2. width 15, Parsing and drawing
  3. consent origin, width 300, Waiting on the network, value 300
  4. consent.js, width 145, Waiting on the network, value 145
  5. width 52, Running script, consent.js and the inline snippet
  6. width 88, Waiting on the network, vendor.js still downloading
  7. vendor.js, width 160, Running script, value 160
  8. width 50, Running script, product.js
  9. width 50, Parsing and drawing, parsing the body
  10. width 40, Running script, async reviews widget runs just before the frame
  11. width 20, Parsing and drawing, style, layout, paint
  • parser held up: 795 ms, from 315 to 1,110
  • Server (time to first byte)
  • Waiting on the network
  • Running script
  • Parsing and drawing
  • Gone after the fix
cmp-run
consent.js and the inline snippet
vendor-wait
vendor.js still downloading
product-run
product.js
body
parsing the body
reviews-run
async reviews widget runs just before the frame
paint
style, layout, paint
Start

As it starts. 2 steps follow.

Copperpot's critical path, each segment in milliseconds. Before, 300 ms of it is the server and only 85 ms the browser parsing and drawing; the other 835 ms is waiting for or running scripts the first frame did not need. Step to the fixed head to see what is left.

Copperpot's milestones before and after

  • Before
  • After
Copperpot's milestones before and afterThe first paint moves from 1,220 to 540 ms and visible text from 1,510 to 540 ms (in the fallback font until 650 ms), while the basket script is ready at 1,110 ms either way, so the page is now visible well before it is fully interactive.0200 ms400 ms600 ms800 ms1 s1.2 s1.4 s1.6 sFirst paintText visibleHero drawnDOMContentLoadedBasket wired1.22 s1.51 s1.22 s1.16 s1.11 s540 ms540 ms910 ms1.12 s1.11 sTime after navigation (ms)MilestoneCopperpot's milestones before and afterThe first paint moves from 1,220 to 540 ms and visible text from 1,510 to 540 ms (in the fallback font until 650 ms), while the basket script is ready at 1,110 ms either way, so the page is now visible well before it is fully interactive.0200 ms400 ms600 ms800 ms1 s1.2 s1.4 s1.6 sFirst …First paintText v…Text visibleHero d…Hero drawnDOMCon…DOMContentLoadedBasket…Basket wired1.22 s1.51 s1.22 s1.16 s1.11 s540 ms540 ms910 ms1.12 s1.11 sTime after navigation (ms)Milestone
Assumed timings from the waterfall above.
Data
MilestoneBefore (ms)After (ms)
First paint1,220540
Text visible1,510540
Hero drawn1,220910
DOMContentLoaded1,1621,115
Basket wired1,1101,110

Finding the blockers on a real page

Record a page load in the Chrome DevTools Performance panel with CPU and network throttling that match your target phone.
Read the head top to bottom and mark every stylesheet whose media matches, every script without async, defer or type=module, and every inline script that sits after a stylesheet.
Check which scripts come from another origin: on a cold visit each new origin adds its own DNS lookup and connection set-up before the first byte.
Look at when the first web font is requested. If it starts after the first paint, the text is invisible or in a fallback until it lands.
In the field, read renderBlockingStatus from Resource Timing entries in Chromium-based browsers to see which resources blocked real users.

Listing render-blocking resources from the page itself

// Run after load, or report it with your field metrics.
const blockers = performance
  .getEntriesByType("resource")
  .filter((e) => e.renderBlockingStatus === "blocking")
  .map((e) => ({ url: e.name, doneAt: Math.round(e.responseEnd) }));

console.table(blockers);
// Copperpot before the fix: main.css 700, consent.js 760, vendor.js 900, product.js 920

Saying it in an interview

Q1
OpeningThe interviewer asks how you would make a product page appear faster. Where does blocking come in?
Start from what the first frame needs: the HTML, the CSS for what is on screen, and nothing else. Then name the waits: matching stylesheets in the head hold the paint, plain scripts hold the parser, and fonts hide text. Say which of the page's scripts the first frame does not need, and load those with defer or async so the paint depends only on HTML and CSS.
Q2
ChoosingWhich script gets defer and which gets async?
defer for the app's own code, which needs the whole DOM and must run in order (a framework before the code that uses it). async for independent third-party code such as analytics, which depends on nothing and nothing depends on. A plain script only when it truly must run before the parser continues, which is rare.
Q3
The catchWhat changes for the user once the page paints before the scripts run?
The page is visible before it is interactive. Copperpot shows its basket button at 0.54 s but its script runs at 1.11 s. Say how the gap is covered: the button is a real form that works without script, or it shows a busy state rather than ignoring the tap. Mention hydration if the page is server-rendered.
Q4
NumbersHow do you show the change worked?
First paint and LCP at p75 on the target phone and network, plus the render-blocking list from DevTools or Resource Timing before and after. On Copperpot, first paint moved from 1.22 s to 0.54 s with no change to how much script ships.

Where the systems' flows lean on this

Product page (Amazon-like store)
The catalog shell is server HTML with the hero as a plain img, so the scanner finds it at once; the buy box and below-the-fold islands are deferred and hydrate later. See amazon/product-detail-page.
Stay listing page (Airbnb-like site)
Streamed HTML puts the head and the gallery in the first chunk; only two islands hydrate at load, and one font subset is preloaded with swap. See airbnb/listing-page.
News feed (Facebook-like web app)
A fast first page of posts: the first post's media is never lazy and gets fetchpriority=high, and nothing the first screen does not need may stand in front of it in the head. See facebook/news-feed.
Workspace boot (Slack-like client)
The app shell comes from a service worker and the data from IndexedDB, so the first frame waits on local files; whatever the shell's head blocks on still decides when it appears. See slack/workspace-boot.
Was this section helpful?

Trade-offs.

Every wait you remove moves a risk somewhere else: a page that paints before its styles or scripts are ready can flash, shift or ignore a tap. Three choices, and how they go wrong.

01
How the app's own scripts load
Chosen:defer (or type=module) in the head
  • Pro:Downloads start with the head; the scanner is not even needed
  • Pro:Never stops the parser
  • Pro:Runs in order after the DOM is complete
  • Pro:Sees every stylesheet's rules
Downside we accept:
  • Con:Waits for parsing to finish
  • Con:so a very long streamed page delays it
  • Con:The page is visible before it is wired up
Ruled out:async

Order between files is not kept; May run before the DOM it needs exists; Can run right before the first frame and push it back

Ruled out:Plain script at the end of body

Download only starts early if the scanner reaches it; Still stops the parser and still waits for CSS; No benefit over defer today

Ruled out:Plain script in the head

Blocks parsing and; because no body exists yet; the first paint; Waits for every stylesheet above it

02
How a third-party tag loadsconsent, analytics, chat
Chosen:async, with the page's own code not depending on it
  • Pro:Its new connection and download happen beside the parser
  • Pro:An outage at the vendor cannot hold the page
Downside we accept:
  • Con:Code that must wait for it (other tags gated on consent) needs a callback or event
  • Con:It can still run at an awkward moment on the main thread
Ruled out:Plain script in the head, as the vendor's snippet says

A cold visit pays the vendor's DNS lookup and connection set-up before the page can render; A slow or down vendor means a white page

Ruled out:Loaded after the first paint or on interaction

Not acceptable for code that must act before other tags (consent); Data from early in the visit may be lost

03
How much CSS blocks the first paint
Chosen:One matching stylesheet for the page, other media split off
  • Pro:Simple and cacheable across pages
  • Pro:No flash of unstyled content
  • Pro:Print and wide-screen rules stop blocking phones
Downside we accept:
  • Con:Rules for parts of the page far below the fold still block the first frame
Ruled out:Inline the CSS for the first screen and load the rest without blocking

Inlined bytes are not cached across pages; Getting the split wrong flashes unstyled content; Belongs to The critical rendering path topic

Ruled out:Load all CSS without blocking

The first paint shows raw HTML and then jumps; Layout shifts hurt CLS

FailureImpactDetectionMitigationMeanwhile
A third-party script in the head hangs (vendor outage, blocked domain)3Network serviceThe parser waits for it, so the page stays white until the browser gives up on the request, which can take many secondsField first paint jumps for every user at once while your own servers look healthyLoad it async, or self-host it, and never make your own code depend on its having runWith async, the page renders and only that vendor's feature is missing
Styles the first screen needs are injected by a script instead of linked in the head4Style engineThe first paint happens before the rules exist, then the page restyles and jumps (unstyled flash, layout shift)CLS in field data, and a filmstrip that shows raw HTML in the first frameKeep every rule the first screen needs in a parser-created stylesheet in the headContent is readable but briefly unstyled
The page paints long before its scripts run5Script engineButtons and inputs look ready but ignore taps, or a tap is lostClicks recorded before the handlers attached; INP and rage-click signals in the first secondsMake key actions work as plain links and forms, show a busy state, and keep the deferred bundle smallActions fall back to a full page request
The web font is slow or fails6RendererWith the default policy, text is invisible for up to 3 s in Chromium and Firefox, then shown in a fallbackA gap between first paint and when text appears in filmstrips; font request timing in Resource TimingPreload the one font the first screen uses and choose a font-display that shows a fallbackText in the fallback face, which may reflow when the font arrives
A script is moved from plain to async but depended on order1HTML parserIt runs before the library it uses, throws, and the feature is dead on some loads but not othersErrors in field monitoring that appear only on fast or cached loadsUse defer for anything that depends on another script; reserve async for independent codeThe feature is missing until the next load wins the race
This topic
What render-blocking and parser-blocking mean, and which tags are which
async, defer, modules and injected scripts
The document's readiness states and DOMContentLoaded
The preload scanner, and why fonts hide text
blocking=render
Elsewhere
The stages a frame goes through once nothing blocks itFrom bytes to pixels
Measuring and shortening the whole critical path, inlining critical CSSThe critical rendering path
preload, preconnect, fetchpriority and choosing font-displayResource hints, priorities and fonts
What a script costs to parse and run once it does runWhat a byte of JavaScript costs
Turning server HTML into a working appHydration, islands and server components
Was this section helpful?
Builds on this
The critical rendering path
Read next