Critical Resource Prioritization: Optimize Loading Order

Medium

Optimizing resource loading order is one of the biggest performance wins - prioritize what users see first.

Quick Decision Guide

Optimal Loading Order:

1. Inline critical CSS - Above-the-fold styles in <head> 2. Preload critical assets - Fonts, hero images 3. Defer non-critical CSS - Load async, apply on load 4. Defer JavaScript - Use defer attribute 5. Lazy load images - Below-fold with loading="lazy"

Result: 50-70% improvement in First Contentful Paint (FCP).

Critical Rendering Path

The Problem

<!-- ❌ Bad: Everything blocks rendering -->
<head>
  <link rel="stylesheet" href="all-styles.css"> <!-- Blocks -->
  <script src="analytics.js"></script> <!-- Blocks -->
  <script src="app.js"></script> <!-- Blocks -->
</head>

Result: Page blank until all resources load (2-4 seconds)

The Solution

<!-- ✅ Good: Prioritize critical path -->
<head>
  <!-- Critical CSS inline -->
  <style>
    /* Only above-the-fold styles */
    body { margin: 0; font-family: sans-serif; }
    .header { background: #fff; height: 60px; }
  </style>
  
  <!-- Preload critical font -->
  <link rel="preload" href="/font.woff2" as="font" crossorigin>
  
  <!-- Defer non-critical CSS -->
  <link rel="preload" href="/styles.css" as="style" 
        onload="this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="/styles.css"></noscript>
  
  <!-- Defer scripts -->
  <script defer src="/app.js"></script>
  <script defer src="/analytics.js"></script>
</head>

Result: Page visible in 0.5-1 second

Critical CSS Strategy

Extract Critical CSS

Use tools to identify above-the-fold styles:

# Using critical package
npm install critical

critical index.html --base dist --inline --minify > index-critical.html

Inline Critical, Defer Rest

<head>
  <!-- Critical styles inline -->
  <style>
    .hero { /* above-fold styles only */ }
    .header { /* ... */ }
  </style>
  
  <!-- Load full stylesheet async -->
  <link rel="preload" href="full-styles.css" as="style" 
        onload="this.onload=null;this.rel='stylesheet'">
  <noscript><link rel="stylesheet" href="full-styles.css"></noscript>
</head>

Next.js Example

// _document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';

class MyDocument extends Document {
  render() {
    return (
      <Html>
        <Head>
          {/* Critical CSS inline */}
          <style dangerouslySetInnerHTML={{
            __html: `
              .hero { /* critical styles */ }
            `
          }} />
          
          {/* Non-critical async */}
          <link rel="preload" href="/styles.css" as="style" 
                onload="this.rel='stylesheet'" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Resource Prioritization

Priority Order

<head>
  <!-- 1. HIGHEST PRIORITY: Critical CSS -->
  <style>/* Inline critical styles */</style>
  
  <!-- 2. HIGH PRIORITY: Preload critical fonts -->
  <link rel="preload" href="/font-regular.woff2" as="font" crossorigin>
  
  <!-- 3. HIGH PRIORITY: Preload hero image -->
  <link rel="preload" href="/hero.jpg" as="image">
  
  <!-- 4. MEDIUM PRIORITY: Preconnect to API -->
  <link rel="preconnect" href="https://api.example.com">
  
  <!-- 5. LOW PRIORITY: Defer non-critical CSS -->
  <link rel="preload" href="/styles.css" as="style" 
        onload="this.rel='stylesheet'">
  
  <!-- 6. LOW PRIORITY: Defer scripts -->
  <script defer src="/app.js"></script>
  <script defer src="/analytics.js"></script>
  
  <!-- 7. LOWEST: Prefetch next page -->
  <link rel="prefetch" href="/dashboard.js">
</head>

Font Loading Strategy

/* Prevent FOIT (Flash of Invisible Text) */
@font-face {
  font-family: 'CustomFont';
  src: url('/font.woff2') format('woff2');
  font-display: swap; /* Show fallback immediately */
}
<!-- Preload critical fonts -->
<link rel="preload" href="/font-bold.woff2" as="font" 
      type="font/woff2" crossorigin>

Script Loading Strategy

Script Priorities

<!-- Critical: Inline small scripts -->
<script>
  // Critical feature detection
  if (!window.fetch) {
    // Load polyfill
  }
</script>

<!-- Important: Defer app code -->
<script defer src="/app.js"></script>

<!-- Low priority: Async analytics -->
<script async src="/analytics.js"></script>

<!-- Lowest: Load on idle -->
<script>
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      const script = document.createElement('script');
      script.src = '/non-critical.js';
      document.body.appendChild(script);
    });
  }
</script>

Third-Party Scripts

<!-- Load after page interactive -->
<script>
  window.addEventListener('load', () => {
    // GTM
    (function(w,d,s,l,i){...})(window,document,'script','dataLayer','GTM-ID');
  });
</script>

Performance Impact

Before Optimization

Timeline:
0ms   - HTML requested
200ms - HTML received
400ms - CSS blocks rendering
800ms - Fonts load (FOIT)
1200ms - JS blocks
1500ms - First paint
2000ms - Interactive

FCP: 1.5s
TTI: 2.0s

After Optimization

Timeline:
0ms   - HTML requested
200ms - HTML received
300ms - Critical CSS (inline) renders
400ms - First paint (fonts with swap)
600ms - Full CSS loads
800ms - JS loads (defer)
1000ms - Interactive

FCP: 0.4s (73% faster)
TTI: 1.0s (50% faster)

Real-World Results

E-commerce Site:

Before: FCP 3.2s, TTI 5.1s
After: FCP 1.1s, TTI 2.3s
Result: 66% faster, 12% higher conversion

News Site:

Before: FCP 2.8s, TTI 4.5s
After: FCP 0.9s, TTI 1.8s
Result: 68% faster, 25% lower bounce rate

Implementation Checklist

Critical Path Optimization

Extract and inline critical CSS

Only above-the-fold styles
Keep under 14KB (first TCP packet)

Preload critical resources

1-2 fonts maximum
Hero/LCP image
Critical JavaScript

Defer non-critical CSS

Load with rel="preload" + onload trick
Fallback with <noscript>

Defer JavaScript

Use defer for app code
Use async for independent scripts
Load analytics after page load

Lazy load images

loading="lazy" for below-fold
Always specify dimensions

Optimize fonts

font-display: swap
Preload critical fonts only
Self-host when possible

Measuring Impact

// Measure First Contentful Paint
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('FCP:', entry.startTime);
  }
}).observe({ entryTypes: ['paint'] });

// Measure Time to Interactive
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('TTI:', entry.processingStart);
  }
}).observe({ entryTypes: ['first-input'] });