Front-end Engineering Lab

Render Blocking Resources

Eliminate resources that block page rendering for faster initial paint

Render Blocking Resources

Render-blocking resources prevent the browser from displaying content. Eliminating or deferring them is critical for fast First Contentful Paint (FCP) and Largest Contentful Paint (LCP).

What Blocks Rendering?

Critical Rendering Path

1. HTML parsing

2. CSS blocks rendering (render-blocking)

3. JavaScript blocks parsing (parser-blocking)

4. Render tree construction

5. Layout

6. Paint

Render-Blocking Resources

✅ Blocks rendering (bad):
- External CSS (<link rel="stylesheet">)
- Synchronous JavaScript (<script src="...">)
- @import in CSS
- Web fonts (FOIT/FOUT)

❌ Doesn't block:
- Images
- Async/defer scripts
- Media queries (non-matching)
- Lazy-loaded resources

CSS Optimization

1. Inline Critical CSS

Extract above-the-fold CSS and inline it:

<!DOCTYPE html>
<html>
<head>
  <!-- Inline critical CSS -->
  <style>
    /* Only styles for above-the-fold content */
    body { margin: 0; font-family: sans-serif; }
    .hero { height: 100vh; background: #000; }
    .nav { position: fixed; top: 0; width: 100%; }
  </style>
  
  <!-- Defer non-critical CSS -->
  <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/styles.css"></noscript>
</head>
<body>
  <!-- Above-the-fold content renders immediately -->
</body>
</html>

2. Extract Critical CSS (Tools)

# Critical (npm package)
npm install critical

# Generate critical CSS
npx critical https://example.com --inline --extract
// critical.config.js
const critical = require('critical');

critical.generate({
  src: 'dist/index.html',
  target: {
    html: 'dist/index-critical.html',
    css: 'dist/critical.css',
  },
  inline: true,
  extract: true,
  width: 1300,
  height: 900,
  penthouse: {
    blockJSRequests: false,
  },
});

3. Media Queries (Non-Blocking)

<!-- Only blocks if media query matches -->
<link rel="stylesheet" href="/print.css" media="print" />
<link rel="stylesheet" href="/mobile.css" media="(max-width: 640px)" />
<link rel="stylesheet" href="/desktop.css" media="(min-width: 1024px)" />

<!-- Loads all, but doesn't block if doesn't match -->

4. Preload + Async Load CSS

<!-- Preload CSS (non-blocking) -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

<!-- Fallback for no-JS -->
<noscript>
  <link rel="stylesheet" href="/styles.css">
</noscript>

5. Split CSS by Route

// next.config.js
module.exports = {
  experimental: {
    optimizeCss: true,  // Enables CSS optimization
  },
};

// Only load CSS for current route
// pages/home.tsx → home.css
// pages/dashboard.tsx → dashboard.css

JavaScript Optimization

1. Async Scripts

<!-- ❌ Blocks parsing -->
<script src="/script.js"></script>

<!-- ✅ Downloads in parallel, doesn't block parsing -->
<script async src="/script.js"></script>

<!-- ✅ Downloads in parallel, executes after parsing -->
<script defer src="/script.js"></script>

Async vs Defer

No attribute (blocking):
HTML parsing → STOP → Download + Execute JS → Resume parsing

async:
HTML parsing → Download JS (parallel) → STOP → Execute → Resume
(Executes as soon as downloaded)

defer:
HTML parsing → Download JS (parallel) → Finish parsing → Execute all defer scripts in order
(Executes after HTML parsed, maintains order)

2. Defer Non-Critical Scripts

<!DOCTYPE html>
<html>
<head>
  <!-- Critical scripts only -->
  <script>
    // Inline critical code
    window.APP_CONFIG = { apiUrl: '/api' };
  </script>
</head>
<body>
  <!-- Content -->
  
  <!-- Non-critical scripts at end with defer -->
  <script defer src="/analytics.js"></script>
  <script defer src="/chat-widget.js"></script>
  <script defer src="/app.js"></script>
</body>
</html>

3. Code Splitting

// Split code by route
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

4. Dynamic Imports

// Load on interaction
async function loadChart() {
  const { Chart } = await import('chart.js');
  const chart = new Chart(/* ... */);
}

// Load on visibility
const observer = new IntersectionObserver((entries) => {
  entries.forEach(async (entry) => {
    if (entry.isIntersecting) {
      const { VideoPlayer } = await import('./VideoPlayer');
      // Render video player
    }
  });
});

Font Optimization

1. font-display: swap

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;  /* Show fallback immediately, swap when loaded */
}

/* Other options:
   block: Wait ~3s, then show fallback (❌ FOIT - Flash of Invisible Text)
   swap: Show fallback immediately, swap when loaded (✅ Recommended)
   fallback: Wait ~100ms, swap if loaded, else show fallback
   optional: Use font only if cached
*/

2. Preload Critical Fonts

<head>
  <!-- Preload critical fonts -->
  <link
    rel="preload"
    href="/fonts/inter-var.woff2"
    as="font"
    type="font/woff2"
    crossorigin
  />
</head>

3. Subset Fonts

# Only include characters you use
npx glyphhanger https://example.com --formats=woff2 --subset=fonts/font.ttf

# Result: Font size reduced from 500KB to 50KB

Next.js Optimization

1. Next.js Font Optimization

import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  preload: true,  // Preload font
  variable: '--font-inter',
});

export default function RootLayout({ children }) {
  return (
    <html className={inter.variable}>
      <body>{children}</body>
    </html>
  );
}

2. Next.js Script Component

import Script from 'next/script';

export default function Page() {
  return (
    <>
      {/* Critical - loads before page is interactive */}
      <Script
        src="/critical.js"
        strategy="beforeInteractive"
      />
      
      {/* After page interactive (defer) */}
      <Script
        src="/analytics.js"
        strategy="afterInteractive"
      />
      
      {/* Lazy - load when idle */}
      <Script
        src="/chat-widget.js"
        strategy="lazyOnload"
      />
    </>
  );
}

Remove Unused CSS

1. PurgeCSS

npm install @fullhuman/postcss-purgecss

# postcss.config.js
module.exports = {
  plugins: [
    require('@fullhuman/postcss-purgecss')({
      content: [
        './pages/**/*.{js,jsx,ts,tsx}',
        './components/**/*.{js,jsx,ts,tsx}',
      ],
      defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [],
      safelist: ['html', 'body'],
    }),
  ],
};

2. UnCSS

npm install uncss

# Remove unused CSS
npx uncss https://example.com -o output.css

3. Tailwind CSS (Built-in Purge)

// tailwind.config.js
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  // Automatically removes unused classes
};

Third-Party Scripts

1. Load Third-Parties After Page Load

// Load analytics after page interactive
useEffect(() => {
  if (typeof window !== 'undefined') {
    const script = document.createElement('script');
    script.src = 'https://www.google-analytics.com/analytics.js';
    script.async = true;
    document.body.appendChild(script);
  }
}, []);

2. Partytown (Web Worker)

npm install @builder.io/partytown

# Move third-party scripts to web worker
import { Partytown } from '@builder.io/partytown/react';

export default function App() {
  return (
    <>
      <Partytown debug={true} forward={['dataLayer.push']} />
      
      {/* Third-party scripts run in web worker */}
      <script type="text/partytown" src="https://www.googletagmanager.com/gtag/js" />
    </>
  );
}

Measuring Render-Blocking

Chrome DevTools

1. Open DevTools
2. Performance tab
3. Record page load
4. Look for:
   - Parse Stylesheet (blocking)
   - Evaluate Script (blocking)
   - Long Tasks (> 50ms)

Lighthouse

npx lighthouse https://example.com --view

# Look for:
# - "Eliminate render-blocking resources"
# - "Reduce unused CSS"
# - "Reduce unused JavaScript"

WebPageTest

https://webpagetest.org

Check:
- Start Render time
- First Contentful Paint
- Render-blocking requests

Best Practices

  1. Inline Critical CSS: Above-the-fold styles only
  2. Defer Non-Critical CSS: Load async after initial render
  3. Async/Defer Scripts: Never block parsing
  4. font-display: swap: Show text immediately
  5. Preload Fonts: Critical fonts only
  6. Code Split: Load only what's needed
  7. Remove Unused CSS: Purge unused styles
  8. Third-Parties Last: Load after page interactive
  9. Media Queries: Non-matching CSS doesn't block
  10. Monitor: Measure with Lighthouse

Real-World Example

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  
  <!-- 1. Preconnect to origins -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://cdn.example.com">
  
  <!-- 2. Preload critical resources -->
  <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
  <link rel="preload" href="/hero.webp" as="image">
  
  <!-- 3. Inline critical CSS -->
  <style>
    /* Above-the-fold styles */
    body{margin:0;font-family:-apple-system,sans-serif}
    .hero{height:100vh;background:#000;color:#fff}
  </style>
  
  <!-- 4. Preload then async load full CSS -->
  <link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/styles.css"></noscript>
</head>
<body>
  <!-- Content renders immediately -->
  <div class="hero">
    <h1>Welcome</h1>
  </div>
  
  <!-- 5. Defer non-critical scripts -->
  <script defer src="/app.js"></script>
  <script defer src="/analytics.js"></script>
</body>
</html>

Common Pitfalls

All CSS in <head>: Blocks render
Inline critical, async load rest

Sync scripts in <head>: Blocks parsing
async/defer or end of <body>

font-display: block: Invisible text
font-display: swap

Bundling everything: Large initial load
Code splitting by route

Third-parties in <head>: Slow FCP
Load after page interactive

Eliminating render-blocking resources is the single biggest improvement you can make to loading performance—prioritize this optimization first!

On this page