Learning on Web Dev Open is free for all.

Take It Apart > What the browser actually doesWhat blocks the paint
Phase 01What the browser actually does35 of 434

What blocks the paint

CSS blocks rendering and a plain script tag blocks parsing. Two facts that explain why a page with almost nothing on it can still take three seconds to show up.

Concept17 minAI adversary

A stylesheet in the head blocks the first paint, deliberately. The alternative is showing unstyled content and then rearranging it in front of the reader, which browsers decided decades ago was worse than a short delay. This is one reason a single slow stylesheet from a third-party domain can hold an otherwise instant page hostage: the browser has all your HTML, it knows what to draw, and it is waiting on someone else’s server before it is willing to draw it.

A plain script tag blocks the parser as well, and for a specific historical reason: the script might call document.write and change what comes next, so the parser cannot safely continue. defer tells the browser the script does not do that, download it in parallel, run it after parsing, in document order. async says run it whenever it lands, in no particular order, which is right for analytics and wrong for almost everything else, because "no particular order" means it can run before the thing it depends on.

The practical rule this collapses into: put stylesheets in the head, put scripts at the end of the body or give them defer, and use async only for things that really do not care when they run. Most pages need nothing more sophisticated than that, and most pages do not do it.

Fonts add their own failure mode, and it is the one that produces bug reports saying "the page was blank". Without font-display, a browser may hide text for up to three seconds waiting for a web font, every byte has arrived, the layout is done, and the reader is looking at nothing. font-display: swap shows the fallback immediately and swaps when the font arrives, trading a flash for text that exists. optional goes further and abandons the web font entirely on a slow connection, which on a body typeface is often the honest choice.

The instrument below is where this becomes muscle memory. Move the app script from the head to the end of the body and watch first paint jump. Add defer to both scripts and watch it jump again. Then notice what is left holding the page: the third-party font stylesheet, which you cannot defer because it is CSS, and so self-hosting fonts is the single most common real-world fix for this whole category of problem.

What is holding up the first paint

first paint 756 msbest possible 274 ms
index.html
<!doctype html>
<head>
<style>/* 1.2 KB of critical CSS */</style>
<link rel="stylesheet" href="/site.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter">
<script src="/analytics.js"></script>
<script src="/app.js"></script>
</head>
<body>
<h1>Pricing</h1>
<p>Three plans, no surprises.</p>
</body>
inline critical CSSinline · no request
/site.cssstylesheet · 120 ms
fonts.googleapis.comstylesheet · 260 ms
/analytics.jsscript · 180 ms
/app.jsscript · 240 ms
Timelinefixed scale, 0 → 1246 ms
HTML parse
critical CSS
site.css
google fonts
analytics.js
app.js
06231246 ms
first paint
756 ms
scripts done
846 ms
words readable
464 ms
font-display
no text drawn until 464 ms

The browser hides the text for up to three seconds waiting for the webfont. Visitors see the layout, the images and the buttons, and no words at all. This is the default.

parser workingparser stalleddownloadingexecutingfirst paint

Everything is in the head and nothing is deferred, which is how most pages start life. First paint is 756 ms. Move a script to the end of the body, or add defer, and watch the marker jump.

A model, not a trace. Real browsers run a preload scanner that starts downloads before the parser reaches the tag, so these fetch bars start later than Chrome’s would. What the model reproduces exactly is who is allowed to block whom, which is the part that decides the number.

Rearrange the tags until first paint stops improving, then look at what is still holding the page.

Do one more thing before you leave: find the best arrangement, then go and apply it to your own deployed page and reload with the cache disabled. The number in your Network panel should move. If it does not, investigate why. That result is more useful than forcing the page to match the example.

Worth reading

You should now be able to

  • Explain why stylesheets block the first paint
  • Choose correctly between defer, async and neither
  • Spot a font loading strategy that causes invisible text
  • Move first paint earlier by rearranging tags rather than deleting code
Ask the community

Loading…