Web Performance & Standards

What is a render-blocking resource?

A render-blocking resource is a file, typically a CSS stylesheet or a synchronous JavaScript file, that the browser must download, parse, and process before it can render any visible content on a web page. When a browser encounters a render-blocking resource while parsing an HTML document it pauses all content rendering, halting the construction of the visual page, until the blocking resource has been fully retrieved from the server and processed. Users see a blank white page during this blocking period, no content is displayed regardless of how much HTML the browser has already parsed.

The browser’s rendering pipeline follows a specific sequence, it constructs the Document Object Model from HTML, constructs the CSS Object Model from CSS, combines them into a render tree, calculates layout positions, and finally paints pixels to the screen. Render-blocking resources interrupt this pipeline, CSS must be fully parsed before the CSSOM is complete and the render tree can be constructed, while synchronous JavaScript can both modify the DOM and query CSS properties, requiring both the DOM construction to pause and any preceding CSS to be fully parsed before the script can execute.

Render-blocking resources are one of the primary causes of poor LCP and slow First Contentful Paint, every millisecond spent downloading and processing blocking resources is a millisecond added to the time before any content appears. Google’s Lighthouse audit tool explicitly identifies render-blocking resources as a performance opportunity, flagging specific files and estimating the LCP savings achievable by eliminating the blocking behaviour.

For redirect management render-blocking resources are a concern for the destination pages that visitors reach through redirects, a redirect already adds TTFB overhead and render-blocking resources on the destination page compound that overhead. Understanding render-blocking resources and how to eliminate them is part of optimising the complete user journey from redirect source to rendered destination.

Why browsers block rendering for CSS

CSS is inherently render-blocking, the browser cannot render any content until it has a complete CSS Object Model, and the browser cannot know when the CSSOM is complete until all CSS has been downloaded and parsed. The reason is fundamental to how CSS works, any CSS rule can affect any element anywhere in the document, a stylesheet loaded late in the document might contain rules that override earlier styles and change the visual appearance of elements that would have already been rendered.

The CSSOM dependency, before the browser can calculate which visual styles apply to each element it needs the complete set of CSS rules. Rendering with incomplete CSS would produce incorrect visual results, elements might display at the wrong size, in the wrong colour, in the wrong position. The browser avoids this by waiting for all CSS to be available before rendering anything, trading render delay for visual correctness.

CSS in the <head> blocks rendering, <link rel="stylesheet"> elements in the HTML <head> are processed synchronously, the browser encounters the stylesheet reference, pauses HTML parsing, fetches the stylesheet, parses the CSS, completes the CSSOM, and only then resumes HTML parsing and proceeds toward rendering. The total blocking time is the sum of the network download time and the CSS parse time for each stylesheet in the <head>.

Critical CSS and the fold, not all CSS is equally important for initial rendering. CSS that affects above-the-fold content, the content visible without scrolling, is needed before the browser can render the initial viewport. CSS that affects only below-the-fold content, which users cannot see until they scroll, is not needed for the initial render. This distinction motivates critical CSS extraction, inlining the CSS needed for above-the-fold content and deferring everything else.

Why browsers block rendering for JavaScript

Synchronous JavaScript in the <head> is render-blocking because JavaScript can both read and modify the DOM and CSSOM, the browser cannot safely proceed with DOM construction or rendering while JavaScript that might change either is pending.

JavaScript and DOM modification, JavaScript can add, remove, and modify DOM elements, document.write() can insert entire HTML fragments into the document. If the browser continued parsing HTML while JavaScript was executing it might build a DOM that the script immediately changes, requiring re-parsing and re-processing of the affected sections. By pausing HTML parsing while synchronous scripts execute the browser ensures the DOM is stable when the script runs.

JavaScript and CSS dependency, JavaScript can query CSS properties, element.style.width, window.getComputedStyle(). If a synchronous script queries CSS properties the browser needs a complete CSSOM to provide accurate values, so the browser must finish processing all preceding CSS before executing the script. A synchronous script in the <head> following a <link rel="stylesheet"> cannot execute until the stylesheet has been fully downloaded and parsed.

Synchronous vs asynchronous scripts, not all script elements block rendering equally.

Synchronous scripts, no async or defer attribute, are fully blocking. The browser pauses HTML parsing and waits for the script to download and execute before proceeding.

async scripts, download in parallel with HTML parsing and execute as soon as they are available, potentially interrupting parsing if the download completes before parsing finishes. Async scripts do not guarantee execution order relative to other scripts, appropriate for independent scripts like analytics that have no dependencies.

defer scripts, download in parallel with HTML parsing but execute only after HTML parsing is complete, in document order. Deferred scripts never block HTML parsing, they execute in a predictable order after the DOM is built. Appropriate for scripts that need access to the DOM and must execute in a specific order.

type="module" scripts, treated as implicitly deferred, behave like defer scripts by default.

Identifying render-blocking resources

Several tools identify render-blocking resources, each providing different levels of detail.

Google Lighthouse, the most accessible render-blocking identification tool. The Eliminate render-blocking resources audit in Lighthouse lists every render-blocking CSS and JavaScript file with an estimated potential LCP savings from eliminating the blocking behaviour. Lighthouse is available in Chrome DevTools, the Performance or Lighthouse panels, and through PageSpeed Insights.

Chrome DevTools Network panel, provides detailed waterfall charts showing the loading sequence of all resources. Render-blocking resources are identifiable by their position at the beginning of the waterfall, before the green First Contentful Paint vertical line. The time between the start of the waterfall and the FCP line represents the total blocking time. Individual resource rows show how long each resource blocked rendering.

Chrome DevTools Performance panel, recording a performance trace shows the main thread timeline, including the Parse HTML, Parse Stylesheet, and Evaluate Script tasks that constitute rendering-blocking work. Long stretches of these tasks before the first paint event indicate significant render-blocking overhead.

WebPageTest, provides detailed waterfall charts with colour-coding that identifies render-blocking resources, shown in orange rather than the green of non-blocking resources. The blocking time is visible in the waterfall as the gap between request start and actual rendering beginning.

Eliminating render-blocking CSS

Several strategies eliminate or reduce CSS render-blocking, the appropriate approach depends on the site architecture and the proportion of CSS needed for above-the-fold content.

Critical CSS inlining, extracting the CSS rules needed to render above-the-fold content and inlining them in a <style> block in the HTML <head>. Inlined CSS is available immediately, no network request required, so above-the-fold content renders as soon as the HTML with the inlined styles is parsed. The remaining non-critical CSS loads asynchronously, available for below-the-fold content when it is needed.

<head>
    <!-- Critical CSS inlined — immediately available -->
    <style>
        body { font-family: sans-serif; margin: 0; }
        .hero { background: #333; color: white; padding: 60px; }
        h1 { font-size: 2rem; }
    </style>
    
    <!-- Non-critical CSS loaded asynchronously -->
    <link rel="preload" as="style" href="/non-critical.css" 
          onload="this.onload=null;this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="/non-critical.css"></noscript>
</head>
<head>
    <!-- Critical CSS inlined — immediately available -->
    <style>
        body { font-family: sans-serif; margin: 0; }
        .hero { background: #333; color: white; padding: 60px; }
        h1 { font-size: 2rem; }
    </style>
    
    <!-- Non-critical CSS loaded asynchronously -->
    <link rel="preload" as="style" href="/non-critical.css" 
          onload="this.onload=null;this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="/non-critical.css"></noscript>
</head>
<head>
    <!-- Critical CSS inlined — immediately available -->
    <style>
        body { font-family: sans-serif; margin: 0; }
        .hero { background: #333; color: white; padding: 60px; }
        h1 { font-size: 2rem; }
    </style>
    
    <!-- Non-critical CSS loaded asynchronously -->
    <link rel="preload" as="style" href="/non-critical.css" 
          onload="this.onload=null;this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="/non-critical.css"></noscript>
</head>

The <link rel="preload" as="style"> with JavaScript-driven rel switching loads the CSS without blocking, the onload handler changes the rel from preload to stylesheet when the file arrives. The <noscript> fallback serves browsers with JavaScript disabled.

CSS media queries for non-screen CSS, CSS files targeted at specific media types or screen conditions can be loaded non-blocking for media that does not match the current context. A print stylesheet does not block screen rendering:

<!-- Does not block screen rendering — only applies to print -->
<link rel="stylesheet" href="/print.css" media="print">

<!-- Does not block for small screens — only applies when condition matches -->
<link rel="stylesheet" href="/large-screen.css" media="(min-width: 1200px)">
<!-- Does not block screen rendering — only applies to print -->
<link rel="stylesheet" href="/print.css" media="print">

<!-- Does not block for small screens — only applies when condition matches -->
<link rel="stylesheet" href="/large-screen.css" media="(min-width: 1200px)">
<!-- Does not block screen rendering — only applies to print -->
<link rel="stylesheet" href="/print.css" media="print">

<!-- Does not block for small screens — only applies when condition matches -->
<link rel="stylesheet" href="/large-screen.css" media="(min-width: 1200px)">

Note, the browser still downloads all CSS files regardless of media query, it just does not block rendering for non-matching media conditions. Media queries prevent blocking but do not prevent downloading.

Reducing CSS size, smaller CSS files download faster, reducing the blocking duration even when blocking cannot be eliminated. Minification reduces CSS file size by 20-30%. Removing unused CSS, CSS rules that apply to no elements on the page, can dramatically reduce file size for large CSS frameworks where only a small fraction of rules are used.

Tree shaking CSS, build tools that analyse which CSS rules are used in the HTML and JavaScript, PurgeCSS, Tailwind CSS’s built-in purging, remove unused rules from the production CSS bundle. A Tailwind CSS project might start with 3MB of utility classes and purge to 10KB of actually used utilities, reducing render-blocking CSS dramatically.

Eliminating render-blocking JavaScript

JavaScript render-blocking is eliminated by changing how scripts are loaded, using async or defer attributes rather than synchronous loading.

Using defer for most scripts, the appropriate approach for most JavaScript that needs to access the DOM or execute in a specific order after page load:

<!-- Blocks rendering — bad for performance -->
<script src="/app.js"></script>

<!-- Deferred — downloads in parallel, executes after HTML parsing -->
<script src="/app.js" defer></script>
<!-- Blocks rendering — bad for performance -->
<script src="/app.js"></script>

<!-- Deferred — downloads in parallel, executes after HTML parsing -->
<script src="/app.js" defer></script>
<!-- Blocks rendering — bad for performance -->
<script src="/app.js"></script>

<!-- Deferred — downloads in parallel, executes after HTML parsing -->
<script src="/app.js" defer></script>

Deferred scripts execute after the HTML is fully parsed but before the DOMContentLoaded event, they have access to the complete DOM and execute in the order they appear in the document.

Using async for independent scripts, appropriate for scripts that have no dependencies on other scripts and do not depend on DOM readiness, analytics, advertising, monitoring:

<!-- Async — downloads in parallel, executes immediately when available -->
<script src="/analytics.js" async></script>
<!-- Async — downloads in parallel, executes immediately when available -->
<script src="/analytics.js" async></script>
<!-- Async — downloads in parallel, executes immediately when available -->
<script src="/analytics.js" async></script>

Async scripts execute as soon as they download, potentially in any order relative to other scripts. Appropriate only for scripts that are completely independent.

Moving scripts to the end of <body>, placing <script> elements at the very end of the <body>, after all HTML content, allows the browser to parse and render all content before encountering the script. The visual content renders before the scripts execute, users see the page while scripts load. This is a legacy technique, defer is generally preferable as it also allows parallel downloading during HTML parsing.

Dynamic script loading, inserting script elements programmatically after page load ensures they never block initial rendering:

// Load script after page is interactive
window.addEventListener('load', () => {
    const script = document.createElement('script')
    script.src = '/non-critical-feature.js'
    document.head.appendChild(script)
})
// Load script after page is interactive
window.addEventListener('load', () => {
    const script = document.createElement('script')
    script.src = '/non-critical-feature.js'
    document.head.appendChild(script)
})
// Load script after page is interactive
window.addEventListener('load', () => {
    const script = document.createElement('script')
    script.src = '/non-critical-feature.js'
    document.head.appendChild(script)
})

Render-blocking and redirect performance

For pages reached through redirects render-blocking resources compound the latency already introduced by the redirect itself.

Compounding TTFB and render-blocking delay, a redirect adds TTFB overhead before the destination page’s HTML begins arriving. If the destination page then has render-blocking CSS that takes 500ms to download the total time before first paint is the redirect TTFB plus destination TTFB plus render-blocking resource download time. Each performance problem multiplies rather than simply adds when combined with others.

LCP on redirect destinations, for pages commonly reached through redirects optimising render-blocking resources on the destination is part of minimising the complete user journey from click to rendered content. Eliminating render-blocking CSS through critical CSS inlining and using defer for all JavaScript removes post-redirect render latency, the destination page begins painting as soon as the HTML arrives rather than waiting for additional resource downloads.

Related terms

Related terms

Ready to keep every link alive?

Ready to keep every link alive?

Ready to keep every link alive?