CodeWithAbdessamad

Lazy Loading Styles

Lazy Loading Styles

In the world of web performance, CSS delivery is a critical factor that impacts the initial load time of your page. While images and other resources can be lazily loaded using the loading="lazy" attribute, CSS is typically loaded synchronously (meaning the browser waits for it to finish before rendering the page). This can lead to a poor user experience if the CSS for non-essential elements is loaded too early.

This section dives into practical techniques for lazy loading CSS — meaning loading non-essential styles only when they become needed, without blocking the initial render. We’ll cover two approaches that are both effective and widely used in production.

Why Lazy Loading CSS Matters

When users first visit a page, browsers prioritize rendering the critical CSS (styles needed to display the above-the-fold content). Non-essential CSS (like styles for sections below the fold, animations, or complex components) can be deferred until later. This reduces:

  • Initial page load time by 30–50% in many cases
  • Time-to-interactive metrics
  • Bandwidth usage
  • Battery drain on mobile devices

The key insight? Don’t load CSS that users don’t need immediately. Lazy loading CSS is about reducing the initial payload while maintaining functionality.

Technique 1: Viewport-Triggered CSS Loading (JavaScript)

This approach loads CSS files only when a specific section becomes visible in the viewport. It’s ideal for pages with multiple sections or complex layouts.

Example implementation:

<code class="language-html"><!-- Critical CSS for above-the-fold content -->
<p><style></p>
<p>  body { font-family: sans-serif; }</p>
<p>  .header { background: #f8f9fa; padding: 1rem; }</p>
<p></style></p>

<p><!-- Non-critical CSS for sections below the fold --></p>
<p><div id="section-2" class="section" style="display: none;"></p>
<p>  <h2>Section 2</h2></p>
<p>  <p class="section-content">This CSS loads when the user scrolls to section 2.</p></p>
<p></div></p>

<p><script></p>
<p>  // Observe when section-2 becomes visible</p>
<p>  const section2 = document.querySelector('.section');</p>
<p>  </p>
<p>  const observer = new IntersectionObserver(</p>
<p>    (entries) => {</p>
<p>      if (entries[0].isIntersecting) {</p>
<p>        // Load CSS when section becomes visible</p>
<p>        const link = document.createElement('link');</p>
<p>        link.rel = 'stylesheet';</p>
<p>        link.href = 'section-2.css';</p>
<p>        document.head.appendChild(link);</p>
<p>        </p>
<p>        // Cleanup</p>
<p>        observer.unobserve(section2);</p>
<p>      }</p>
<p>    },</p>
<p>    { threshold: 0.1 }</p>
<p>  );</p>
<p>  </p>
<p>  observer.observe(section2);</p>
<p></script></code>

Why this works:

  1. Critical CSS is loaded upfront (visible immediately)
  2. Non-critical CSS loads only when the section enters the viewport
  3. Uses the native IntersectionObserver API (no extra libraries)
  4. Maintains smooth scrolling performance

Best for: Multi-section pages, progressive web apps, or sites with heavy visual content below the fold.

Technique 2: preload with onload (CSS-First Approach)

This technique uses the browser’s built-in preload mechanism to fetch CSS files early while the page is loading, then applies them when needed.

Example implementation:

<code class="language-html"><!-- Critical CSS for above-the-fold content -->
<p><style></p>
<p>  body { font-family: sans-serif; }</p>
<p>  .header { background: #f8f9fa; padding: 1rem; }</p>
<p></style></p>

<p><!-- Non-critical CSS for sections below the fold --></p>
<p><div id="section-2" class="section" style="display: none;"></p>
<p>  <h2>Section 2</h2></p>
<p>  <p class="section-content">This CSS loads via preload.</p></p>
<p></div></p>

<p><script></p>
<p>  // Preload CSS for section 2</p>
<p>  const section2Link = document.createElement('link');</p>
<p>  section2Link.rel = 'preload';</p>
<p>  section2Link.as = 'style';</p>
<p>  section2Link.href = 'section-2.css';</p>
<p>  document.head.appendChild(section2Link);</p>

<p>  // Apply CSS when section becomes visible</p>
<p>  const section2 = document.querySelector('.section');</p>
<p>  </p>
<p>  const observer = new IntersectionObserver(</p>
<p>    (entries) => {</p>
<p>      if (entries[0].isIntersecting) {</p>
<p>        // Apply the preloaded CSS</p>
<p>        section2Link.rel = 'stylesheet';</p>
<p>        section2Link.href = 'section-2.css';</p>
<p>        section2Link.onload = () => {</p>
<p>          section2Link.rel = 'preload'; // Reset for next time</p>
<p>        };</p>
<p>      }</p>
<p>    },</p>
<p>    { threshold: 0.1 }</p>
<p>  );</p>
<p>  </p>
<p>  observer.observe(section2);</p>
<p></script></code>

Why this works:

  1. Uses the browser’s built-in preload for efficient caching
  2. Applies styles only when the section becomes visible
  3. Minimizes network requests (avoids duplicate fetches)
  4. Works without JavaScript execution for the CSS itself

Best for: Sites with high network latency, mobile-first designs, or when minimizing JS overhead is critical.

Key Comparison: When to Use Which

Technique When to Use Pros Cons
Viewport-Triggered (JS) Complex layouts, many sections, high visual weight No extra network requests, full control Requires JavaScript, slightly heavier init
preload + onload Mobile-first sites, low bandwidth, minimal JS Leverages browser optimizations, faster Requires careful management of preload

Critical insight: Both techniques avoid the common pitfall of loading CSS before the user needs it. The difference lies in how early you fetch the CSS and when you apply it.

Best Practices

  1. Extract critical CSS first using tools like critical or webpack-optimization
  2. Use IntersectionObserver instead of scroll events for better performance
  3. Prioritize sections that users are likely to scroll to first (e.g., after the header)
  4. Test with Lighthouse to verify CSS performance metrics
  5. Avoid async/defer for CSS — they cause unpredictable rendering order

💡 Pro tip: For maximum performance, combine both techniques: Use preload for CSS that will be needed early, and viewport-triggered loading for the rest.

Summary

Lazy loading CSS is about reducing the initial payload while maintaining functionality. By strategically loading non-essential styles only when they become visible in the viewport (using IntersectionObserver), or via browser-native preload with onload, you can significantly improve page speed without sacrificing user experience.

Key Takeaway: Always prioritize critical CSS upfront, then defer non-essential styles using viewport-triggered or preload techniques. This approach reduces initial load time by up to 50% while keeping your page responsive and accessible.