Render-Blocking Resources: How to Speed Up First Paint Without Sacrificing Functionality
What render-blocking CSS and JS are, why they hurt LCP, and which practical techniques eliminate them — preload, async/defer, critical CSS, code splitting.
"The site takes forever to load" — the most common user complaint and the most common cause of ranking drops in SEO. And in 80% of cases, there is a single culprit: render-blocking resources. These are CSS and JS files that the browser must download and process before it can draw anything on the screen for the user.
This article covers what render-blocking resources are, how to find them, and which techniques eliminate them without sacrificing site functionality.
How Page Rendering Works
To understand the problem, you need to understand how the browser builds a page.
- The browser requests the HTML
- Parses the HTML, builds the DOM tree
- Encounters
<link rel="stylesheet">or<script src="">in<head> - Stops parsing and goes to download the resource
- Downloads it, processes it (parses CSS, executes JS)
- Returns to parsing the HTML
- Finally builds the CSSOM, merges it with the DOM into the Render Tree
- Performs Layout (geometry), Paint, Compositing
- The user sees the first frame
If the resource at steps 3–5 is slow to download, the user stares at a white screen. LCP (Largest Contentful Paint) bloats. Bounce rate rises.
A render-blocking resource is any CSS or JS that the browser must download and process before rendering begins.
How to Find Render-Blocking Resources
Open PageSpeed Insights on your site's homepage. Under "Opportunities" there is usually a line reading "Eliminate render-blocking resources" with a list of files and how much time can be saved.
Also:
- Chrome DevTools → Network — reload the page. Files of type script and stylesheet that are loaded BEFORE the
DOMContentLoadedevent are render-blocking. - Chrome DevTools → Coverage (open via Cmd+Shift+P → "Show Coverage"). Shows what percentage of each resource was actually used on the current page. If you have 200 KB of CSS but only 15 KB is used — there is room to optimise.
- Lighthouse → Audits → Performance — the same as PageSpeed but running locally.
Technique 1: async and defer for JS
This is the most fundamental optimisation that everyone knows about but not everyone applies correctly.
<script src="analytics.js"></script> <!-- blocking -->
<script src="analytics.js" async></script> <!-- downloads in parallel, executes when ready -->
<script src="analytics.js" defer></script> <!-- downloads in parallel, executes after DOMContentLoaded -->
When to use async:
- Analytics (Yandex.Metrica, GA, Hotjar)
- Social media widgets
- Any JS that does not depend on the DOM or other scripts
When to use defer:
- JS that works with the DOM (finds elements, attaches event handlers)
- JS that must execute in a specific order (multiple defer scripts execute sequentially in the order they appear in the HTML)
- Polyfills and heavy libraries
When to leave it blocking:
- Very rarely. Only if the script is critically needed to render above-the-fold content and CSS cannot substitute for it.
Technique 2: Critical CSS Inlined in HTML
CSS blocks rendering differently from JS — the browser must wait for the complete CSSOM before the first paint. This means even a small blocking CSS file adds +300–500 ms to LCP on a slow connection.
The solution: critical CSS — styles needed only for the above-the-fold block are inlined directly in <head> via <style>. The remaining CSS is loaded asynchronously.
<head>
<style>
/* Critical CSS: header, hero, main buttons. Typically 8–15 KB. */
body { font: 16px/1.5 sans-serif; margin: 0; }
.header { padding: 20px; background: #fff; }
.hero { padding: 60px 20px; text-align: center; }
/* ... */
</style>
<!-- Remaining CSS — asynchronously -->
<link rel="preload" href="/styles/main.css" as="style"
onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/styles/main.css"></noscript>
</head>
The challenge: critical CSS changes whenever the design changes. Maintaining it by hand is impractical. Use automated tools instead:
- Critters (npm package, integrates into the build)
- PurgeCSS + Critical for static sites
- Next.js handles this automatically via CSS Modules + RSC
- Astro inlines critical CSS from default scope automatically
Technique 3: Code Splitting in JS
If you have a single large bundle.js weighing 800 KB, it blocks or slows the loading of every page — even where only 50 KB of functionality is needed.
The solution: split the code into chunks by route and feature.
In Next.js this works out of the box — each page gets its own chunk plus a shared common chunk. For heavy components, go further:
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
ssr: false,
loading: () => <Skeleton />
});
HeavyChart.js will not be included in the main bundle — it will be loaded as a separate request only when the page needs to display it.
In React without Next.js — React.lazy() + Suspense:
const HeavyChart = React.lazy(() => import('./HeavyChart'));
<Suspense fallback={<Skeleton />}>
<HeavyChart />
</Suspense>
In Vue 3 — defineAsyncComponent.
Technique 4: Preload Key Resources
If a resource is definitely going to be needed for rendering, you can tell the browser to start downloading it immediately without waiting for HTML parsing to reach it.
<head>
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/hero-image.jpg" as="image">
</head>
This is especially important for:
- Fonts (without preload, the browser only discovers
@font-facein CSS after the CSS loads — causing FOUT/FOIT) - The main hero image (LCP candidate)
- Critical JS chunks (if using code splitting)
Do not overuse preload — each preload steals priority from other resources. 3–5 preloads per page is normal; 30 is degradation.
Technique 5: Third Parties Under Control
Third parties are the most common source of render-blocking resources in 2026. Analytics platforms, ad networks, live chat widgets, social media embeds. Each widget means +1 synchronous JS file at 50–200 KB.
Strategies:
- Audit: identify every third party loaded on the site. For each one, ask: "Is it actually needed?" Often 60% can be removed without any loss.
- Async for everyone: add
asyncordeferto all third-party scripts. - Lazy loading: some widgets can be loaded only when needed. For example, an Intercom chat widget can be loaded after the user's first interaction, not immediately on page load.
- Self-hosting analytics: instead of
<script src="https://google-analytics.com/...">— proxy it through your own domain. It will be faster (one TCP handshake), more reliable (not subject to ad blockers), and more privacy-friendly. - Tag Manager only if genuinely needed: Google Tag Manager adds +200 KB to every page. If you only have 3 scripts, it is simpler to include them directly.
Technique 6: HTTP/2 and HTTP/3
This is an infrastructure-level change, but it directly affects render-blocking behaviour.
In HTTP/1.1, the browser is limited to 6 parallel requests to a single domain. If you have 10 resources in the head, 4 of them wait in the queue. Render-blocking time accumulates.
In HTTP/2 — multiplexing means a single TCP connection transmits all resources simultaneously. In HTTP/3 (QUIC) — there is additionally faster recovery from packet loss.
What you need to do:
- Enable HTTP/2 in nginx (it is available by default from version 1.13+, check for
listen 443 ssl http2;) - HTTP/3 is still somewhat experimental, but nginx 1.25+ supports it:
listen 443 ssl http2 http3; - CDN — most modern CDNs (Cloudflare, BunnyCDN, KeyCDN) support HTTP/3 out of the box
Effect: on sites with 20+ resources in the head, enabling HTTP/2 alone can reduce LCP by 200–500 ms.
A Real-World Example
A typical site before optimisation:
- 1 main CSS file — 145 KB, render-blocking
- 1 jQuery — 88 KB, render-blocking
- 1 main.js — 320 KB, render-blocking
- 4 third-party resources (analytics, chat, Hotjar, ads) — 280 KB combined
- LCP: 4.2 sec (mobile, 4G)
After optimisation:
- Critical CSS inlined at 12 KB + main.css preloaded at 145 KB
- jQuery removed entirely (it was not used in the code)
- main.js — deferred, split into 6 chunks
- All third-party resources — async or removed
- LCP: 1.8 sec
Result: bounce rate dropped by 32%, conversions increased by 18%, Yandex rankings across 60% of queries improved by an average of 4 positions.
Summary
Render-blocking resources are the primary cause of a slow first paint. They are eliminated through a combination of techniques: async/defer for JS, critical CSS inline, code splitting, preload, third-party control, and HTTP/2.
Most of these are one-time optimisations that then continue working reliably. An hour of an engineer's time can save users hours of combined waiting.
Want a performance audit? Contact us — a free express audit delivered within 2 business days.