Render Blocking Resources

TL;DR / CSS and synchronous JavaScript block the browser from rendering — the page stays blank until these resources are downloaded, parsed, and executed.

How It Works

 Parser encounters resources

 ┌─────────────┐          ┏━━━━━━━━━━━━┓
 │ HTML Parser │─────────→┃ <link css> ┃   BLOCKS render
 └─────────────┘          ┗━━━━━━━━━━━━┛


                          ┏━━━━━━━━━━━━┓
                          ┃  <script>  ┃   BLOCKS parse + render
                          ┗━━━━━━━━━━━━┛


                          ┌────────────────┐
                          │ <script async> │   downloads in parallel
                          └────────────────┘


                          ┌────────────────┐
                          │ <script defer> │   runs after parse complete
                          └────────────────┘

Edit diagram

A render-blocking resource is any file that must be fully downloaded and processed before the browser can paint pixels. The two primary offenders are CSS stylesheets and synchronous JavaScript. Until these resources are resolved, the user sees a blank page — not partially styled content, not unstyled content, but nothing.

How CSS Blocks Rendering

When the HTML parser encounters a <link rel="stylesheet">, it initiates a fetch for that CSS file. The parser continues building the DOM (CSS does not block parsing), but the browser will not construct the render tree or paint until the CSSOM is complete. This is intentional — rendering with partial CSS would produce a flash of incorrectly styled content that immediately snaps to the correct layout, which is a worse user experience than a brief blank page.

This means every CSS file linked in <head> is render-blocking. A single large stylesheet or a chain of @import rules can add hundreds of milliseconds to First Contentful Paint, especially on slow connections.

How JavaScript Blocks Parsing and Rendering

A synchronous <script> tag (without async or defer) blocks the HTML parser entirely. When the parser encounters one, it must:

  1. Pause DOM construction.
  2. If any CSS preceding the script hasn't been parsed yet, wait for CSSOM construction (because the script might query computed styles).
  3. Fetch the script (if external).
  4. Execute the script.
  5. Resume parsing.

This creates a double dependency: the script depends on CSS completing, and DOM construction depends on the script completing. A large stylesheet followed by a large synchronous script can compound into significant delays.

Loading Strategies

async downloads the script in parallel with parsing. When the download completes, the parser pauses to execute the script. This means async scripts execute in download-completion order, not document order. Use async for independent scripts that do not depend on DOM structure or other scripts — analytics, ads, and third-party widgets.

defer downloads the script in parallel with parsing but defers execution until after the DOM is fully parsed, just before DOMContentLoaded. Deferred scripts execute in document order. Use defer for scripts that need the full DOM but do not need to block rendering.

type="module" scripts are deferred by default. They also support async for eager execution. Module scripts respect the dependency graph, so imports are fetched in parallel where possible.

Inline scripts (<script>/* code */</script>) do not require a network fetch but still block parsing during execution. If the code is small, the blocking time is negligible. If it is large, consider extracting it to an external deferred script.

Eliminating Render-Blocking CSS

Inline critical CSS. Extract the CSS needed for above-the-fold content and embed it directly in a <style> block in <head>. This eliminates the network round-trip for initial rendering. The full stylesheet loads asynchronously for the rest of the page.

Load non-critical CSS asynchronously. The media attribute trick — <link rel="stylesheet" href="print.css" media="print" onload="this.media='all'"> — tells the browser the stylesheet is not needed for the current media type, so it does not block rendering. On load, the media is switched back, applying the styles.

Use <link rel="preload"> for critical CSS. Preloading hints tell the browser to start fetching the resource immediately, before the parser discovers the <link> tag. This reduces the time CSS spends on the critical path by starting the fetch earlier.

Split CSS by route. Instead of one monolithic stylesheet, generate per-page or per-component CSS. Code-splitting for CSS ensures only the styles needed for the current view are on the critical path.

Measuring Render-Blocking Impact

Chrome DevTools' Coverage tab shows how much of each CSS and JS file is actually used during page load. Large percentages of unused code indicate opportunities to split or defer. The Network tab's waterfall chart reveals blocking chains — look for CSS files that must complete before scripts can execute.

Lighthouse flags specific render-blocking resources with estimated savings. The "Eliminate render-blocking resources" audit identifies CSS and JS files that delay First Contentful Paint and suggests async, defer, or inlining strategies.

Gotchas

  • All CSS is render-blocking by default, regardless of how small the file is. Even a 1KB stylesheet blocks rendering until it is downloaded and parsed. The network latency, not the file size, is usually the bottleneck.
  • @import creates hidden waterfalls. The browser cannot discover the imported stylesheet until it has downloaded and parsed the parent. This serializes what could be parallel downloads. Always use <link> tags in HTML instead.
  • Third-party scripts are often the worst offenders. Analytics, chat widgets, and ad scripts frequently use synchronous loading. Audit third-party tags and convert them to async or defer wherever possible.
  • document.write() forces the parser to synchronize. Scripts using document.write() cannot be deferred because they modify the parser's input stream. Chrome blocks document.write() for cross-origin scripts on slow connections entirely.
  • Preload without as does not prioritize correctly. <link rel="preload" href="style.css"> without as="style" may be fetched at the wrong priority. Always include the as attribute to tell the browser how to prioritize the fetch.