Render-Blocking Resources — Eliminating Blocks Guide
In this tutorial, you will learn how to identify, analyze, and eliminate render-blocking resources that delay the first paint of your web pages. Render-blocking resources are CSS and JavaScript files that the browser must download, parse, and execute before it can render any content. DodaTech eliminated 90 percent of render-blocking resources on its main site, reducing FCP by 1.2 seconds.
What You Will Learn
- How the browser determines which resources are render-blocking
- How to identify render-blocking resources using Lighthouse and DevTools
- How to eliminate render-blocking CSS with inlining and async loading
- How to eliminate render-blocking JavaScript with defer and async
Why It Matters
Render-blocking resources are the number one cause of slow first paint. Each blocking resource adds at least one network round trip to the critical path. Google Lighthouse flags every blocking resource as an opportunity, and fixing them often produces the largest performance gains.
Real-World Use Case
Durga Antivirus Pro dashboard loaded Bootstrap CSS and jQuery as render-blocking resources. After inlining the critical CSS and deferring jQuery, First Contentful Paint dropped from 2.8 seconds to 1.1 seconds. The security dashboard became usable much faster for administrators.
Prerequisites
You should understand Critical Rendering Path concepts and how the CSS Object Model is constructed. Familiarity with JavaScript script loading behavior is necessary.
Step-by-Step Tutorial
Step 1: Identify Render-Blocking Resources
Open Chrome DevTools, run a Lighthouse report, and check the Opportunities section. Lighthouse lists all render-blocking resources with estimated time savings.
# Lighthouse CLI to find blocking resources
npx lighthouse https://example.com --output json --quiet \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
items = d['audits']['render-blocking-resources']['details']['items']
for i in items:
print(f\"{i['url']} - {i['wastedMs']}ms\")
"
Expected output: A list of URLs with estimated savings. For example:
https://example.com/style.css- 380mshttps://example.com/app.js- 260ms
Step 2: Understand What Makes a Resource Blocking
A CSS or JavaScript resource is render-blocking when:
- CSS: Any
<link rel="stylesheet">in the<head>blocks rendering. The browser pauses DOM construction until the CSSOM is built. - JavaScript: Any
<script>tag withoutdeferorasyncblocks DOM construction if placed before the CSSOM is ready.
<!-- Blocking: parser halts until styles.css is downloaded and parsed -->
<link rel="stylesheet" href="styles.css">
<!-- Blocking: parser halts until app.js is downloaded and executed -->
<script src="app.js"></script>
<!-- Non-blocking: deferred execution -->
<script src="app.js" defer></script>
<!-- Non-blocking: async execution -->
<script src="analytics.js" async></script>
Step 3: Eliminate Blocking CSS
There are two complementary approaches:
Approach A: Inline Critical CSS
Extract the CSS needed for above-the-fold content and inline it in the <head>. Load the remaining CSS asynchronously.
<head>
<!-- Inlined critical CSS - renders immediately -->
<style>
header { background: #1a1a2e; padding: 1rem; }
.hero { font-size: 2rem; color: #e94560; }
/* Only above-the-fold styles */
</style>
<!-- Non-critical CSS loaded asynchronously via preload -->
<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>
Approach B: Split CSS by Media Type
Use media queries to mark CSS as non-blocking.
<!-- Blocking on all devices -->
<link rel="stylesheet" href="main.css">
<!-- Non-blocking: only blocks printing -->
<link rel="stylesheet" href="print.css" media="print">
<!-- Non-blocking: only loads when orientation matches -->
<link rel="stylesheet" href="portrait.css" media="orientation:portrait">
Step 4: Eliminate Blocking JavaScript
Apply the correct script loading attribute based on when the script is needed.
<!-- Blocking: parser halts -->
<script src="app.js"></script>
<!-- Deferred: downloads during parsing, executes after parsing (preserves order) -->
<script src="app.js" defer></script>
<!-- Async: downloads during parsing, executes immediately after download (no order) -->
<script src="analytics.js" async></script>
For scripts that must load before the page renders (critical rendering scripts), inline them directly.
<head>
<script>
// Critical rendering logic - inlined to avoid a blocking request
function loadTheme() {
const theme = localStorage.getItem('theme') || 'dark';
document.documentElement.setAttribute('data-theme', theme);
}
loadTheme();
</script>
</head>
Step 5: Use the Script Loading Decision Tree
Use this decision tree for each script on your page:
- Is this script needed before rendering? → Inline it
- Is this script independent with no order requirements? → Use
async - Does this script need to preserve execution order? → Use
defer - Is this script only needed on user interaction? → Use dynamic
import()
// Dynamic import for interaction-triggered scripts
document.getElementById('load-chart').addEventListener('click', async () => {
const { Chart } = await import('chart.js');
// Initialize chart...
});
Step 6: Analyze the Critical Path Depth
Count the number of round trips in your critical path. Each blocking resource adds one round trip.
// Calculate critical path depth
const blockingResources = [
'HTML document (1 RTT)',
'critical.css (1 RTT - if not inlined)',
'critical.js (1 RTT - if not inlined)'
];
console.log(`Critical path depth: ${blockingResources.length} round trips`);
Goal: Reduce the critical path to 1 round trip (the HTML itself). Everything else should load in parallel.
Step 7: Automate Blocking Resource Detection
Add a check to your CI pipeline to flag new render-blocking resources.
// Custom script to check for blocking resources
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.coverage.startCSSCoverage();
await page.goto('https://example.com');
const cssCoverage = await page.coverage.stopCSSCoverage();
cssCoverage.forEach(entry => {
const usedBytes = entry.text.length *
entry.ranges.reduce((sum, r) => sum + r.end - r.start, 0) / entry.text.length;
const usedPercent = (usedBytes / entry.text.length * 100).toFixed(1);
console.log(`${entry.url}: ${usedPercent}% used`);
if (usedPercent < 50) {
console.log(` WARNING: Less than 50% of this CSS is used above the fold`);
}
});
await browser.close();
})();
Step 8: Measure the Improvement
Compare before and after metrics using the Performance API.
// Measure first paint time
window.addEventListener('load', () => {
const paintEntries = performance.getEntriesByType('paint');
paintEntries.forEach(entry => {
console.log(`${entry.name}: ${entry.startTime}ms`);
});
// Expected output:
// first-paint: 210ms (before: 850ms)
// first-contentful-paint: 230ms (before: 920ms)
});
Learning Path
flowchart LR A[Critical Rendering Path] --> B[Render-Blocking Resources] B --> C[Preload, Prefetch, Preconnect] B --> D[CSS Performance] C --> E[Performance Budgets] style B fill:#4f46e5,color:#fff style A fill:#6366f1,color:#fff style C fill:#6366f1,color:#fff
Common Errors
Treating all CSS as blocking: Not all CSS needs to be blocking. Use media attributes to make print or orientation-specific CSS non-blocking.
Using async for all scripts: Async scripts execute as soon as they download, which can happen in the middle of DOM construction. Use async only for independent scripts like analytics.
Inlining large CSS blocks: Inlining the entire stylesheet (e.g., 50KB of CSS) delays HTML parsing. Only inline critical above-the-fold CSS, which is typically 2-8KB.
Putting scripts in the head without defer: Scripts in the
<head>withoutdeferorasyncare the worst offenders. Move scripts to the end of<body>or adddefer.Not inlining critical JavaScript: JavaScript that affects the initial render (theme detection, redirects, A/B testing) should be inlined to avoid blocking requests.
Ignoring inline event handlers: Inline event handlers like
onclick="handler()"are not blocking themselves, but they prevent the browser from caching the handling logic separately.
Practice Questions
- What determines whether a CSS resource is render-blocking?
- What is the difference between async and defer for script loading?
- Why does inlining critical CSS improve render performance?
- How can media attributes make CSS non-blocking?
- What is the critical path depth and why does it matter?
Answers: 1. A CSS resource blocks rendering if it is a <link rel="stylesheet"> in the <head> without a media attribute that evaluates to false. 2. async executes as soon as the script downloads; defer executes after HTML parsing completes in document order. 3. Inlined CSS requires no network request and is available immediately when the parser encounters it. 4. By adding media="print" or media="orientation:portrait", the browser knows the CSS is not needed for the current state and makes it low-priority or non-blocking. 5. The number of sequential network requests needed before the first paint; each additional round trip increases time to first paint.
Challenge
Audit a production page and list all render-blocking resources. Calculate the total potential time savings. Apply the following optimizations: inline critical CSS, defer all non-critical JavaScript, add media queries to theme CSS files, and convert async-incompatible scripts to defer. Run Lighthouse before and after to measure the FCP and LCP improvement.
FAQ
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro