Denis Kirevby Denis Kirev

Understanding the Critical Rendering Path

Deep dive into browser rendering pipeline and optimization techniques for better performance.

5 min read
PERFORMANCEWEBGUIDE

🚀 What is the Critical Rendering Path?

The Critical Rendering Path (CRP) is the sequence of steps the browser takes to convert HTML, CSS, and JavaScript into pixels on the screen. Understanding and optimizing this process is crucial for improving web application performance, accelerating page rendering, and ensuring a smooth user experience.

🔍 Key Stages of CRP

1. Building the DOM (Document Object Model)

The browser parses HTML into a tree structure representing the page content:

<!-- HTML -->
<html>
  <head>
    <title>Example</title>
  </head>
  <body>
    <div>
      <h1>Hello</h1>
      <p>World</p>
    </div>
  </body>
</html>

<!-- Becomes DOM Tree -->
html ├── head │ └── title └── body └── div ├── h1 └── p

2. Constructing the CSSOM (CSS Object Model)

CSS is parsed into its own tree structure. This process is render-blocking:

/* CSS */
body { font-size: 16px; }
div { padding: 20px; }
h1 { color: blue; }
p { margin: 10px 0; }

/* Becomes CSSOM Tree */
body
├── font-size: 16px
└── div
    ├── padding: 20px
    ├── h1
    │   └── color: blue
    └── p
        └── margin: 10px 0

3. Creating the Render Tree

The browser combines DOM and CSSOM to create a render tree containing only visible elements:

// Elements with display: none are excluded
const renderTree = {
  nodeName: 'body',
  styles: { fontSize: '16px' },
  children: [
    {
      nodeName: 'div',
      styles: { padding: '20px' },
      children: [
        {
          nodeName: 'h1',
          styles: { color: 'blue' },
          content: 'Hello',
        },
        {
          nodeName: 'p',
          styles: { margin: '10px 0' },
          content: 'World',
        },
      ],
    },
  ],
}

4. Layout (Reflow)

The browser calculates exact positions and dimensions of each element:

// Example of layout calculations
function calculateLayout(element) {
  const computedStyle = getComputedStyle(element)
  const width =
    element.offsetWidth + parseInt(computedStyle.marginLeft) + parseInt(computedStyle.marginRight)
  const height =
    element.offsetHeight + parseInt(computedStyle.marginTop) + parseInt(computedStyle.marginBottom)

  return { width, height }
}

5. Paint

The final step where visual elements are drawn on the screen.

🧱 DOM and CSSOM: The Foundation

DOM Construction

  • Incremental process as HTML is parsed
  • Can be modified by JavaScript
  • Blocked by synchronous scripts
// DOM manipulation example
document.addEventListener('DOMContentLoaded', () => {
  // Safe to manipulate DOM here
  const element = document.createElement('div')
  element.textContent = 'Dynamic content'
  document.body.appendChild(element)
})

CSSOM Construction

  • Render-blocking resource
  • All CSS must be processed before rendering
  • Complex selectors can slow down CSSOM construction
/* Avoid complex selectors */
.header .nav ul li a.active span {
} /* ❌ Too specific */
.nav-link-active {
} /* ✅ Better */

/* Use media queries to make non-critical CSS non-blocking */
@media print {
  /* These styles won't block rendering */
}

🌲 Render Tree Construction

The browser combines DOM and CSSOM, excluding invisible elements:

<!-- Input HTML -->
<div class="container">
  <h1 style="display: none">Hidden</h1>
  <p>Visible content</p>
</div>

<!-- Render Tree (conceptual) -->
<div class="container">
  <p>Visible content</p>
</div>

📐 Layout: Element Positioning

Layout performance considerations:

// ❌ Causes multiple reflows
element.style.width = '100px'
element.style.height = '100px'
element.style.margin = '10px'

// ✅ Better: batch style changes
element.style.cssText = 'width: 100px; height: 100px; margin: 10px;'
// Or use classes
element.classList.add('new-dimensions')

🎨 Paint: The Final Step

Optimize paint performance:

/* ❌ Triggers layout and paint */
.element {
  left: 100px;
  top: 50px;
}

/* ✅ Only triggers compositing */
.element {
  transform: translate(100px, 50px);
}

🛠 Optimization Strategies

1. Minimize Critical Resources

<!-- Defer non-critical JavaScript -->
<script defer src="non-critical.js"></script>

<!-- Load CSS conditionally -->
<link rel="stylesheet" href="print.css" media="print" />

<!-- Preload critical resources -->
<link rel="preload" href="critical.css" as="style" />
<link rel="preload" href="hero-image.jpg" as="image" />

2. Reduce File Sizes

// Use code splitting
import('./features/chat').then(module => {
  // Chat functionality loaded on demand
})

// Minimize CSS with critical path extraction
const criticalCSS = extractCritical(html)

3. Optimize Loading Order

<!-- Preconnect to required origins -->
<link rel="preconnect" href="https://api.example.com" />

<!-- Inline critical CSS -->
<style>
  /* Critical styles here */
</style>

<!-- Async load non-critical CSS -->
<link rel="stylesheet" href="non-critical.css" media="print" onload="this.media='all'" />

4. Avoid Frequent Reflows

// ❌ Multiple reflows
const list = document.querySelector('.list')
items.forEach(item => {
  list.appendChild(createListItem(item))
})

// ✅ Single reflow
const fragment = document.createDocumentFragment()
items.forEach(item => {
  fragment.appendChild(createListItem(item))
})
list.appendChild(fragment)

📏 Measurement Tools

// Performance API usage
performance.mark('startOperation')
// ... perform operation
performance.mark('endOperation')

performance.measure('operationDuration', 'startOperation', 'endOperation')

// Get measurements
const measures = performance.getEntriesByType('measure')
console.log(measures)

🚀 Best Practices Summary

  1. Critical CSS Extraction
// Example using critical package
const critical = require('critical')

critical.generate({
  base: 'dist/',
  src: 'index.html',
  target: {
    css: 'critical.css',
    html: 'index-critical.html',
  },
  width: 1300,
  height: 900,
})
  1. Resource Hints
<link rel="preconnect" href="https://api.example.com" />
<link rel="dns-prefetch" href="https://cdn.example.com" />
<link rel="preload" href="critical.js" as="script" />
  1. Performance Monitoring
// Monitor layout thrashing
const observer = new PerformanceObserver(list => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'layout-shift') {
      console.log('Layout shift detected:', entry)
    }
  }
})

observer.observe({ entryTypes: ['layout-shift'] })

💡 Conclusion

Understanding and optimizing the Critical Rendering Path is crucial for building fast, responsive web applications. Key takeaways:

  • Minimize and optimize critical resources
  • Reduce render-blocking resources
  • Implement efficient DOM operations
  • Use modern performance APIs for monitoring
  • Regular testing with Chrome DevTools and Lighthouse

Remember: measure first, then optimize. Use tools like Lighthouse, Chrome DevTools, and WebPageTest to identify actual bottlenecks in your rendering pipeline.


📚 Further Reading

For more detailed information about the Critical Rendering Path, check out the MDN Web Docs guide.

Last updated: January 16, 2025