How to Find What Blocks Your Site's First Paint

How to Find What Blocks Your Site's First Paint

Render blocking resources are the scripts, stylesheets and fonts that the browser must fetch and process before it can paint anything on screen. To find them, open your page in a browser's network panel or run a synthetic audit, then read the request waterfall: anything that finishes before the first paint and sits in the critical path is a candidate. This article shows you how to identify those requests precisely and how to change them so the page paints sooner without breaking behaviour.

Why a stylesheet or script stops the first paint

The mechanism is in the HTML parser. When it reaches a <link rel="stylesheet"> element, it must download and parse the CSS before it renders anything, because the stylesheet could change the layout of content already parsed. A plain <script src="..."> is worse: the parser stops, fetches the file, executes it, and only then continues. That is why a single blocking script in the <head> can delay everything after it, including the body content the user is waiting to see.

Fonts are a subtler case. A web font declared with @font-face is not fetched until a rule matches text on the page, but once it is requested, text using that font may be invisible until the file arrives, depending on how the font is declared. The fix is a CSS descriptor rather than a markup change. The browser also has hints you can use: preload starts a fetch early, and preconnect opens a connection to a third party origin before you need it.

Find render blocking resources with the browser and the terminal

The quickest way to see the critical path is to throttle the connection and watch what arrives before the first paint. In Chrome or Firefox, open developer tools, switch to the Network tab, set throttling to a slow profile, and reload. The waterfall shows each request's start and finish relative to the first paint marker. Look for CSS and synchronous scripts that finish after the document but before any pixels appear.

For a scriptable check, use a headless browser or a command line audit tool. The exact flags vary by tool, so check the documentation for the version you have installed. A typical invocation and the shape of its output look like this:

$ npx some-audit-tool https://example.test/ --only=render-blocking
  URL                          TYPE   TRANSFER  BLOCKING
  /css/main.css                css    12.4 kB   yes
  /js/app.js                   script 48.1 kB   yes
  /fonts/inter.woff2           font   22.0 kB   no
  first paint: 2.1s

If you prefer raw data, fetch the document and list every external resource in order. This does not tell you what blocks paint, but it gives you the candidate list to test against the waterfall:

$ curl -s https://example.test/ \
    | grep -Eo '(src|href)="[^"]+"' \
    | sort -u
src="/js/app.js"
href="/css/main.css"
href="/fonts/inter.woff2"

Cross check that list against the network waterfall. A resource is only blocking if it appears in the critical path before first paint. A stylesheet loaded with media="print" or a script with defer or async will not block in the same way, so read the attributes in the HTML before you conclude anything.

Fix render blocking resources without breaking the page

Start with scripts, because they are usually the easiest to move without changing behaviour. If a script does not need to run before the page is interactive, add defer so it downloads in parallel and executes after parsing. If it is independent of other scripts and the DOM, async is fine. Keep the order of dependent scripts by using defer on all of them rather than mixing the two attributes.

For stylesheets, split the critical CSS from the rest. Inline the rules needed for above the fold content in a <style> block, and load the full sheet without blocking using the media trick or a preload that swaps to a stylesheet on load. A common pattern is to load the full sheet as a preload with an onload handler that sets rel="stylesheet". Always keep a <noscript> fallback so the page still has styles if scripts are disabled:

<link rel="preload" href="/css/main.css" as="style"
     >
<noscript><link rel="stylesheet" href="/css/main.css"></noscript>

Fonts need a different approach. Add font-display: swap to every @font-face rule so text renders in a fallback font immediately and swaps when the file arrives. Preload the font file if it is used above the fold, and host it on the same origin as the page to avoid an extra connection. A minimal declaration looks like this:

@font-face {
  font-family: "Inter";
  src: url("/fonts/inter.woff2") format("woff2");
  font-display: swap;
}

After each change, re run the audit and compare the first paint time and the blocking list. Change one thing at a time so you can attribute the difference. Watch for regressions in layout and interaction, especially if you moved a script that depended on being parsed early.

What to do next

Take the blocking list from your last audit and work through it in order of transfer size, starting with scripts. Add defer or async where behaviour allows, inline the critical CSS, and set font-display: swap on every font rule. Then re run the audit on a throttled connection and confirm the first paint moved earlier. Keep the audit in your release checklist so a new blocking script does not creep back in unnoticed.

Related articles

Subscribe to our newsletter

Get the latest hosting tips, performance insights, and industry news.