Skip to main content

Overview

The critical rendering path is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into a rendered page. Optimizing this path is crucial for fast initial page loads and excellent user experience.
The critical rendering path directly impacts First Contentful Paint (FCP) and Largest Contentful Paint (LCP), two key Core Web Vitals metrics.

Understanding the rendering path

The browser follows these steps to render a page:
1

Parse HTML

The browser parses HTML to construct the DOM (Document Object Model) tree.
2

Parse CSS

CSS files are parsed to construct the CSSOM (CSS Object Model) tree.
3

Execute JavaScript

JavaScript can modify both DOM and CSSOM, potentially blocking rendering.
4

Construct render tree

DOM and CSSOM are combined to create the render tree (only visible elements).
5

Layout

Calculate exact position and size of each element.
6

Paint

Draw pixels to the screen.
CSS is render-blocking by default. JavaScript is both parser-blocking and render-blocking. These are the primary optimization targets.

Eliminate render-blocking resources

Render-blocking resources prevent the browser from displaying content until they’re downloaded and processed.

Identifying render-blocking resources

Run a Lighthouse audit and check the “Eliminate render-blocking resources” opportunity.
lighthouse https://example.com --view
Look for:
  • Stylesheets in <head>
  • Synchronous <script> tags
  • Large CSS files
  • Unused CSS rules

Async and defer scripts

<!-- Blocks HTML parsing until downloaded and executed -->
<script src="script.js"></script>

<div>This content waits for script.js</div>
Use async for scripts that:
  • Don’t depend on other scripts
  • Don’t modify the DOM before it’s ready
  • Can execute in any order
Examples: Analytics, ads, social media widgets
<script src="https://www.googletagmanager.com/gtag/js" async></script>
<script src="//connect.facebook.net/en_US/sdk.js" async></script>
Use defer for scripts that:
  • Need the full DOM to be parsed
  • Depend on other scripts (execution order matters)
  • Can wait until HTML parsing completes
Examples: Main application code, UI libraries, DOM manipulation
<script src="jquery.js" defer></script>
<script src="app.js" defer></script>
<!-- app.js executes after jquery.js -->
Use blocking scripts (no async/defer) only when:
  • Script must execute before page renders
  • Other scripts depend on it immediately
Examples: Critical polyfills, A/B testing that prevents FOUC
<!-- Critical: must run before rendering -->
<script src="critical-polyfill.js"></script>

Minimize critical CSS

Critical CSS is the minimum CSS needed to render above-the-fold content.

Extracting critical CSS

1

Identify critical styles

Determine which CSS rules are needed for above-the-fold content.
2

Inline critical CSS

Place critical CSS directly in the <head> to eliminate the network request.
3

Defer non-critical CSS

Load remaining CSS asynchronously to avoid blocking rendering.

Implementation approaches

<!DOCTYPE html>
<html>
  <head>
    <!-- Inline critical CSS -->
    <style>
      /* Critical styles for header, hero, above-fold content */
      body { margin: 0; font-family: system-ui; }
      .header { height: 60px; background: #fff; }
      .hero { height: 400px; background: #f0f0f0; }
    </style>
    
    <!-- Async load full stylesheet -->
    <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 -->
  </body>
</html>

Async CSS loading pattern

<!-- Preload CSS file -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

<!-- Fallback for browsers without JavaScript -->
<noscript>
  <link rel="stylesheet" href="styles.css">
</noscript>
Use media queries to load CSS conditionally:
<link rel="stylesheet" href="print.css" media="print">
<link rel="stylesheet" href="mobile.css" media="(max-width: 768px)">
Browsers still download all stylesheets but only block rendering for matching media queries.

Defer non-critical JavaScript

Defer JavaScript that’s not essential for initial rendering to improve page load performance.

Lazy loading JavaScript

// Load module only when needed
button.addEventListener('click', async () => {
  const module = await import('./heavy-feature.js');
  module.initialize();
});

// Conditional loading
if (window.innerWidth > 768) {
  import('./desktop-features.js').then(module => {
    module.init();
  });
}

Third-party script optimization

Download and serve third-party scripts from your own domain to improve caching and reduce DNS lookups.
<!-- Before: external CDN -->
<script src="https://cdn.example.com/library.js"></script>

<!-- After: self-hosted -->
<script src="/js/library.js" defer></script>
Benefits:
  • Control over caching headers
  • No third-party DNS lookup
  • Better privacy
  • Reduced connection overhead
Replace heavy third-party embeds with lightweight facades that load the real thing on interaction.
import { useState } from 'react';

function YouTubeEmbed({ videoId }) {
  const [loaded, setLoaded] = useState(false);
  
  if (!loaded) {
    return (
      <div
        className="youtube-facade"
        style={{
          backgroundImage: `url(https://i.ytimg.com/vi/${videoId}/hqdefault.jpg)`,
          cursor: 'pointer'
        }}
        onClick={() => setLoaded(true)}
      >
        <div className="play-button"></div>
      </div>
    );
  }
  
  return (
    <iframe
      src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
      allow="autoplay"
      allowFullScreen
    />
  );
}
Saves ~500KB by loading YouTube player only on click.
Load analytics, social widgets, and ads after critical content.
// Load after window load event
window.addEventListener('load', () => {
  // Analytics
  const analyticsScript = document.createElement('script');
  analyticsScript.src = 'https://www.google-analytics.com/analytics.js';
  document.head.appendChild(analyticsScript);
  
  // Social widgets
  const fbScript = document.createElement('script');
  fbScript.src = 'https://connect.facebook.net/en_US/sdk.js';
  fbScript.async = true;
  document.body.appendChild(fbScript);
});
Third-party scripts can significantly impact performance. Audit them regularly and remove unused ones.

Preload, prefetch, and preconnect strategies

Resource hints tell the browser to optimize loading for specific resources.

Resource hint types

Preload

Load critical resources early in the page lifecycle.Priority: High
Use for: Critical fonts, CSS, scripts, images

Prefetch

Load resources needed for future navigation during idle time.Priority: Low
Use for: Next page resources, likely user actions

Preconnect

Establish early connections to important third-party origins.Priority: Medium
Use for: API domains, CDNs, fonts

DNS-prefetch

Resolve DNS early for third-party domains.Priority: Low
Use for: Multiple third-party domains

Preload examples

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

<link
  rel="preload"
  href="/fonts/roboto-mono.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

<style>
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/inter-var.woff2') format('woff2');
    font-display: swap;
  }
</style>
Always include crossorigin attribute for font preloads, even for same-origin fonts.

Prefetch examples

<!-- Prefetch next page -->
<link rel="prefetch" href="/page2.html" />

<!-- Prefetch route chunks -->
<link rel="prefetch" href="/js/dashboard-chunk.js" />

<!-- Prefetch API data -->
<link rel="prefetch" href="/api/user-data.json" />

<!-- Prefetch images for next page -->
<link rel="prefetch" href="/images/page2-hero.webp" as="image" />
Prefetch resources during idle time:
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    const link = document.createElement('link');
    link.rel = 'prefetch';
    link.href = '/dashboard';
    document.head.appendChild(link);
  });
}

Preconnect examples

<!-- Connect to API domain early -->
<link rel="preconnect" href="https://api.example.com" />

<!-- Connect to CDN -->
<link rel="preconnect" href="https://cdn.example.com" />

<!-- Connect to Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

Resource hint decision tree

1

Is it critical for initial render?

Yes → Use preloadNo → Go to step 2
2

Will the user likely need it soon?

Yes → Use prefetchNo → Go to step 3
3

Does it require a third-party connection?

Yes → Use preconnect or dns-prefetchNo → Don’t use resource hints
Don’t overuse resource hints. Too many preloads can delay actual critical resources. Limit to 3-5 preloads per page.

Complete optimization example

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Optimized Page</title>
    
    <!-- Preconnect to critical origins -->
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link rel="preconnect" href="https://api.example.com" />
    
    <!-- Preload critical fonts -->
    <link
      rel="preload"
      href="/fonts/inter.woff2"
      as="font"
      type="font/woff2"
      crossorigin
    />
    
    <!-- Preload hero image (LCP element) -->
    <link
      rel="preload"
      href="/images/hero.webp"
      as="image"
      type="image/webp"
      fetchpriority="high"
    />
    
    <!-- Inline critical CSS -->
    <style>
      /* Critical above-the-fold styles */
      body{margin:0;font-family:Inter,system-ui,sans-serif}
      .header{height:60px;background:#fff;box-shadow:0 2px 4px rgba(0,0,0,.1)}
      .hero{height:400px;background:url(/images/hero.webp) center/cover}
    </style>
    
    <!-- Async load full stylesheet -->
    <link
      rel="preload"
      href="/css/styles.css"
      as="style"
      onload="this.onload=null;this.rel='stylesheet'"
    />
    <noscript><link rel="stylesheet" href="/css/styles.css" /></noscript>
    
    <!-- Preload critical script -->
    <link rel="modulepreload" href="/js/app.js" />
  </head>
  
  <body>
    <header class="header">
      <!-- Header content -->
    </header>
    
    <section class="hero">
      <!-- Hero content -->
    </section>
    
    <!-- Main content -->
    <main>
      <!-- Content -->
    </main>
    
    <!-- Deferred main script -->
    <script type="module" src="/js/app.js"></script>
    
    <!-- Async analytics -->
    <script async src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"></script>
    
    <!-- Prefetch next likely page -->
    <link rel="prefetch" href="/dashboard" />
  </body>
</html>

Measuring impact

Lighthouse Score: 45/100

FCP: 3.2s
LCP: 5.8s
TTI: 7.2s
CLS: 0.15

Render-blocking resources: 4 (2.1s)
- main.css (850ms)
- fonts.css (320ms)
- app.js (680ms)
- analytics.js (250ms)

Critical rendering path checklist

  • Minimize HTML size
  • Enable compression (gzip/brotli)
  • Use early hints (103 status code)
  • Optimize server response time (TTFB < 200ms)
  • Extract and inline critical CSS
  • Async load non-critical CSS
  • Remove unused CSS
  • Minimize CSS file size
  • Use media queries for conditional loading
  • Defer non-critical scripts
  • Use async for independent scripts
  • Minimize JavaScript execution time
  • Code split and lazy load
  • Optimize third-party scripts
  • Preload critical resources
  • Preconnect to required origins
  • Prefetch likely next resources
  • Optimize font loading
  • Lazy load below-fold images
  • Lighthouse score > 90
  • FCP < 1.8s
  • LCP < 2.5s
  • TTI < 3.8s
  • Monitor real user metrics (RUM)

Next steps

Profiling tools

Use Chrome DevTools and Lighthouse to identify optimization opportunities

Optimization techniques

Apply memoization, virtualization, and code splitting for better performance

Build docs developers (and LLMs) love