Identify and eliminate render-blocking CSS and JavaScript that delay FCP and LCP — with concrete code fixes for defer, async, preload, and critical CSS.
PageSpeed Insights flags "Eliminate render-blocking resources" more often than almost any other audit item. It's also one of the most misunderstood. Developers see the warning, add defer to one script, and wonder why the score didn't move. The reason: render-blocking isn't one problem, it's five overlapping problems with different fixes.
This post is a systematic walkthrough. By the end you'll be able to identify every blocking resource on a page, prioritize which to fix, and hand a developer a specific remediation path for each.
The browser builds the DOM and CSSOM before it can paint anything. Any resource that interrupts that construction delays the First Contentful Paint (FCP) and, by extension, the Largest Contentful Paint (LCP).
CSS is render-blocking by default. The browser won't paint until it has finished downloading and parsing every stylesheet in the <head>. JavaScript is also render-blocking by default because it can modify the DOM — the browser pauses HTML parsing when it hits a <script> tag with no defer or async attribute.
The practical consequence: a single 80KB stylesheet loaded from a slow CDN, or one analytics script loaded synchronously, can add 300–800ms to FCP on a mobile connection.
Run the Page Speed Grader or open PageSpeed Insights directly. Look for the "Eliminate render-blocking resources" opportunity. Expand it — each blocking resource is listed with its estimated savings.
Also check the waterfall in the Network tab of Chrome DevTools. Set throttling to "Fast 3G." Look for any requests that start before the first paint (vertical dashed line) and haven't finished — those are your candidates.
The Coverage tab (Cmd+Shift+P → "Show Coverage") shows what percentage of each loaded CSS/JS file was actually used on the page. Files with 70%+ unused bytes are strong candidates for splitting or removal.
defer downloads the script in parallel with HTML parsing and executes it after the DOM is built, in order. Use defer for scripts that need to run in sequence.
async downloads in parallel and executes immediately when downloaded, out of order. Use async for completely independent scripts like tag managers, heatmap tools, or comment widgets that have no dependencies.
What not to do:defer a script that another defer'd script depends on and expects to already be in the global scope. That breaks execution order. Use module bundling instead.
For legacy setups where defer isn't viable (inline scripts, third-party embeds with no defer support), moving <script> tags just before </body> achieves a similar effect — the DOM is fully parsed before the script executes.
<!-- end of page content --> </main> <script src="/assets/legacy-widget.js"></script></body>
This doesn't help with download timing — the script still downloads late — but it eliminates the parsing block.
The correct fix for render-blocking CSS is to inline the CSS needed to render above-the-fold content directly in <style> tags in the <head>, then load the full stylesheet asynchronously.
The rel="preload" as="style" with the onload swap is the canonical async CSS loading pattern. It downloads the stylesheet immediately (high priority) but doesn't block rendering, then swaps it in once downloaded.
Tools for extracting critical CSS: Critical (Node.js), Penthouse, or Critters (used by Angular CLI and Gatsby by default). Run these as part of your build process, not manually.
Third-party scripts are the most common source of render-blocking resources on client sites, and they're the hardest to fix because the client often owns the business decision to include them.
Typical culprits and their costs:
Script
Typical blocking cost
Alternative
Synchronous Google Tag Manager
100–300ms
Ensure GTM is loaded via async snippet
Facebook Pixel (sync)
80–150ms
Load with defer or via GTM
Live chat (sync)
150–400ms
Defer until user interaction
WordPress jetpack scripts
200–500ms
Disable unused Jetpack modules
Font Awesome (full kit)
90–200ms
Use a subset or migrate to system icons
The fix for GTM specifically: GTM's default snippet already uses async. If someone added GTM as a plain <script src> without the async snippet, replace it with the correct async version from the GTM interface.
For live chat tools (Intercom, Drift, Zendesk), use facade patterns: load a placeholder button, then inject the real widget script only when the user clicks the button or after the page reaches idle state using requestIdleCallback.
// Load chat widget after page idlewindow.addEventListener('load', function() { requestIdleCallback(function() { var script = document.createElement('script'); script.src = 'https://widget.intercom.io/widget/YOUR_APP_ID'; document.head.appendChild(script); });});
This splits vendor libraries into a separate chunk that can be cached separately and prevents the full app bundle from loading on pages that don't need it.
The Coverage tab often reveals stylesheets where 60–90% of the rules never apply to the current page. Common sources: Bootstrap loaded in full when only the grid is used, icon font CSS for all 600 icons when 12 are used, a plugin stylesheet for a plugin that was deactivated without removing its enqueue.
PurgeCSS (for build pipelines) and UnCSS scan HTML and remove any CSS rule that doesn't match an element on the page. Use with caution — dynamic class names (added by JavaScript) must be safeguarded with an allowlist or they'll be purged.
For WordPress sites, the Asset CleanUp or Perfmatters plugins let you dequeue specific stylesheets on specific pages without modifying theme files.
FCP is the first paint. LCP is the largest paint — usually the hero image or headline. Everything that delays FCP also delays LCP. But there's a compounding factor: if the LCP element is an <img> that's discovered late in the HTML because parsing was blocked, the browser starts downloading it late. A 500ms render-blocking penalty at the start can translate to an 800ms LCP penalty at the end.
This is why fixing render-blocking resources has an outsized impact on LCP scores. It's not just about the blocking time itself — it's about how early the browser discovers and prioritizes the LCP candidate.
Don't add <link rel="preload"> for every asset on the page. Preloading competes for bandwidth. Preload only the LCP image, critical fonts, and the asynchronously-loaded main stylesheet. Over-preloading degrades performance.
Don't inline an entire stylesheet as "critical CSS." Critical CSS should be 10–15KB maximum. If your above-the-fold styles require more than that, the page has a design complexity problem, not a CSS problem.
The audit one-liner for render-blocking resources: Defer every non-critical script, inline the critical CSS for above-the-fold content, load the rest asynchronously, and eliminate any third-party script that isn't actively earning its blocking cost in revenue.