Performance Optimization Advanced 12 min read

Critical CSS and Above-the-Fold Optimization

Optimize first paint in your React Shopify theme. Learn to extract critical CSS, defer non-essential styles, and ensure content displays before JavaScript loads.

The fastest JavaScript in the world can’t help if users are staring at a blank screen waiting for styles to load. Critical CSS is the minimum CSS needed to render above-the-fold content—the part of the page users see before scrolling. By inlining this CSS and deferring the rest, you dramatically reduce time to first paint.

Understanding the Critical Rendering Path

Before the browser can display your page, it must:

┌─────────────────────────────────────────────────────────────────┐
│ CRITICAL RENDERING PATH │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Parse HTML ─────────────────────────────────────────────────│
│ └─ Build DOM tree │
│ │
│ 2. Fetch CSS (BLOCKING) ───────────────────────────────────────│
│ └─ Wait for all stylesheets │
│ └─ Parse CSS │
│ └─ Build CSSOM │
│ │
│ 3. Combine DOM + CSSOM ────────────────────────────────────────│
│ └─ Create render tree │
│ │
│ 4. Layout + Paint ─────────────────────────────────────────────│
│ └─ Calculate positions │
│ └─ Paint pixels │
│ │
│ CSS IS RENDER-BLOCKING │
│ Nothing displays until CSS is loaded and parsed! │
│ │
└─────────────────────────────────────────────────────────────────┘

The solution: inline critical CSS so it’s available immediately, then load the rest asynchronously.

Identifying Critical CSS

Critical CSS includes styles for everything visible without scrolling:

┌─────────────────────────────────────────────────────────────────┐
│ VIEWPORT (Above the Fold) ← Critical CSS │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ HEADER ││
│ │ [Logo] [Nav] [Cart] [Search] ││
│ └─────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ HERO / PRODUCT IMAGE ││
│ │ ││
│ │ [Main Content] ││
│ │ ││
│ └─────────────────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ PRODUCT INFO (partial) ││
│ │ Title, Price, First part of description ││
│ └─────────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────────┤
│ BELOW THE FOLD ← Deferred CSS │
│ ───────────────────────────────────────────────────────────────│
│ Product tabs, reviews, recommendations, footer │
│ Cart drawer, search modal, quick view │
└─────────────────────────────────────────────────────────────────┘

For a Shopify theme, critical CSS typically includes:

  • CSS variables and resets
  • Header and navigation styles
  • Hero/banner styles
  • Product image container
  • Product title and price
  • Basic typography
  • Layout grid/flexbox

Extracting Critical CSS Manually

For precise control, manually create a critical CSS file:

src/styles/critical.css
/* CSS Variables - needed everywhere */
:root {
--color-primary: #2563eb;
--color-text: #1f2937;
--color-background: #ffffff;
--font-body: system-ui, sans-serif;
--font-heading: system-ui, sans-serif;
--header-height: 64px;
}
/* Minimal reset */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-body);
color: var(--color-text);
background: var(--color-background);
line-height: 1.5;
}
/* Header - always visible */
.header {
position: sticky;
top: 0;
z-index: 100;
height: var(--header-height);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1rem;
background: var(--color-background);
border-bottom: 1px solid #e5e7eb;
}
.header__logo img {
height: 40px;
width: auto;
}
.header__nav {
display: flex;
gap: 1.5rem;
}
.header__actions {
display: flex;
gap: 0.5rem;
}
/* Main content area */
.main {
min-height: calc(100vh - var(--header-height));
}
/* Product page critical styles */
.product-page {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
@media (max-width: 768px) {
.product-page {
grid-template-columns: 1fr;
}
}
.product-gallery__main {
aspect-ratio: 1;
background: #f3f4f6;
}
.product-gallery__main img {
width: 100%;
height: 100%;
object-fit: contain;
}
.product-title {
font-size: 1.875rem;
font-weight: 600;
line-height: 1.2;
}
.product-price {
font-size: 1.5rem;
font-weight: 600;
margin-top: 0.5rem;
}
/* Skeleton placeholders (shown before React loads) */
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-pulse 1.5s infinite;
}
@keyframes skeleton-pulse {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}

Inlining Critical CSS in Liquid

Add critical CSS directly in your theme’s <head>:

{% comment %} layout/theme.liquid {% endcomment %}
<!DOCTYPE html>
<html lang="{{ request.locale.iso_code }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ page_title }}</title>
{%- comment -%} Critical CSS - inlined for fastest first paint {%- endcomment -%}
<style>
{{ 'critical.css' | asset_url | stylesheet_tag | remove: '<link' | remove: '/>' | remove: 'rel="stylesheet"' | remove: 'href="' | split: '"' | first | strip }}
</style>
{%- comment -%} Or inline directly if the above doesn't work {%- endcomment -%}
{%- style -%}
{% render 'critical-styles' %}
{%- endstyle -%}
{%- comment -%} Non-critical CSS - loaded asynchronously {%- endcomment -%}
<link rel="preload" href="{{ 'theme.css' | asset_url }}" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="{{ 'theme.css' | asset_url }}"></noscript>
</head>

The preload with onload trick loads CSS without blocking render.

Automated Critical CSS Extraction

For larger projects, automate critical CSS extraction:

Terminal window
npm install -D critical

Create a build script:

scripts/extract-critical.ts
import critical from 'critical';
import { readFileSync, writeFileSync } from 'fs';
const pages = [
{ url: 'https://your-store.myshopify.com/', output: 'critical-home.css' },
{ url: 'https://your-store.myshopify.com/products/sample', output: 'critical-product.css' },
{ url: 'https://your-store.myshopify.com/collections/all', output: 'critical-collection.css' },
];
async function extractCritical() {
for (const page of pages) {
console.log(`Extracting critical CSS for ${page.url}...`);
const { css } = await critical.generate({
src: page.url,
width: 1300,
height: 900,
// Also extract for mobile
dimensions: [
{ width: 375, height: 667 }, // Mobile
{ width: 1300, height: 900 }, // Desktop
],
// Inline in the HTML
inline: false,
// Minify output
minify: true,
// Extract from these stylesheets
css: ['dist/assets/theme.css'],
});
writeFileSync(`assets/${page.output}`, css);
console.log(` → Saved ${css.length} bytes to ${page.output}`);
}
}
extractCritical();

Template-Specific Critical CSS

Different pages need different critical CSS:

{% comment %} layout/theme.liquid {% endcomment %}
<head>
{%- comment -%} Base critical CSS for all pages {%- endcomment -%}
<style>{% render 'critical-base' %}</style>
{%- comment -%} Template-specific critical CSS {%- endcomment -%}
{%- case template.name -%}
{%- when 'product' -%}
<style>{% render 'critical-product' %}</style>
{%- when 'collection' -%}
<style>{% render 'critical-collection' %}</style>
{%- when 'index' -%}
<style>{% render 'critical-home' %}</style>
{%- when 'cart' -%}
<style>{% render 'critical-cart' %}</style>
{%- endcase -%}
</head>

CSS Loading Strategy

Implement a comprehensive CSS loading strategy:

{% comment %} snippets/css-loader.liquid {% endcomment %}
{%- comment -%}
CSS Loading Strategy:
1. Critical CSS - inlined in <head>
2. High-priority CSS - preloaded
3. Low-priority CSS - loaded async after paint
{%- endcomment -%}
{%- comment -%} 1. Critical CSS (already in head) {%- endcomment -%}
{%- comment -%} 2. High-priority: fonts and icons {%- endcomment -%}
<link rel="preload" href="{{ 'fonts.css' | asset_url }}" as="style">
<link rel="stylesheet" href="{{ 'fonts.css' | asset_url }}" media="print" onload="this.media='all'">
{%- comment -%} 3. Main stylesheet - deferred {%- endcomment -%}
<link rel="preload" href="{{ 'theme.css' | asset_url }}" as="style">
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}" media="print" onload="this.media='all'">
{%- comment -%} 4. Low-priority: animations, effects {%- endcomment -%}
<link rel="stylesheet" href="{{ 'animations.css' | asset_url }}" media="print" onload="this.media='all'">
{%- comment -%} Fallback for no-JS {%- endcomment -%}
<noscript>
<link rel="stylesheet" href="{{ 'fonts.css' | asset_url }}">
<link rel="stylesheet" href="{{ 'theme.css' | asset_url }}">
<link rel="stylesheet" href="{{ 'animations.css' | asset_url }}">
</noscript>

Handling CSS-in-JS with React

If using CSS modules or styled-components with React:

// For CSS Modules - styles are bundled with JS
// Extract critical styles server-side or use the inlined approach
// For styled-components with SSR-like behavior
import { ServerStyleSheet } from 'styled-components';
// But in Shopify themes, we don't have SSR for React
// So use this hybrid approach:
// 1. Critical layout styles in regular CSS (inlined in Liquid)
// 2. Component styles in CSS Modules (loaded with JS)
// 3. Skeleton styles in critical CSS to prevent layout shift

Preventing Layout Shift

Ensure content doesn’t jump when full CSS loads:

critical.css
/* Reserve space for elements that load async */
.product-gallery {
aspect-ratio: 1;
background: #f3f4f6;
}
.product-thumbnails {
display: flex;
gap: 0.5rem;
height: 80px;
}
.product-form {
min-height: 200px; /* Reserve space for variant selectors */
}
/* Placeholder for React-rendered content */
[data-react-root] {
min-height: 100px;
}
/* Hide content that will be hydrated */
[data-react-root]:empty::before {
content: '';
display: block;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-pulse 1.5s infinite;
height: 100%;
min-height: inherit;
}

Optimizing Web Fonts

Fonts are often the biggest contributor to render delay:

critical.css
/* Use font-display: swap to show text immediately */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* Show fallback immediately, swap when loaded */
}
/* Or use font-display: optional to skip if slow */
@font-face {
font-family: 'CustomFont';
src: url('/fonts/custom.woff2') format('woff2');
font-display: optional; /* Use if cached, skip if not */
}

Preload critical fonts:

<head>
{%- comment -%} Preload critical fonts {%- endcomment -%}
<link rel="preload" href="{{ 'custom-font.woff2' | asset_url }}" as="font" type="font/woff2" crossorigin>
</head>

Measuring First Paint Improvements

Track your optimizations:

src/lib/performance-metrics.ts
export function measureFirstPaint() {
if (!('PerformanceObserver' in window)) return;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-paint') {
console.log(`First Paint: ${entry.startTime.toFixed(0)}ms`);
}
if (entry.name === 'first-contentful-paint') {
console.log(`First Contentful Paint: ${entry.startTime.toFixed(0)}ms`);
}
}
});
observer.observe({ type: 'paint', buffered: true });
}
export function measureLCP() {
if (!('PerformanceObserver' in window)) return;
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
console.log(`Largest Contentful Paint: ${lastEntry.startTime.toFixed(0)}ms`);
console.log(`LCP Element:`, lastEntry.element);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
}

Key Takeaways

  1. CSS blocks rendering: Nothing displays until CSS is loaded. Inline critical CSS to eliminate this delay.

  2. Identify above-the-fold: Only styles for visible content need to be critical. Everything else can defer.

  3. Load async: Use media="print" onload="this.media='all'" to load non-critical CSS without blocking.

  4. Template-specific critical CSS: Different pages need different critical styles.

  5. Reserve space: Use aspect ratios and min-heights to prevent layout shift.

  6. Optimize fonts: Use font-display: swap or optional to show text immediately.

  7. Measure impact: Track First Paint, FCP, and LCP to verify improvements.

  8. Automate extraction: Use tools like critical to generate critical CSS from live pages.

In the next lesson, we’ll cover performance monitoring and Core Web Vitals to ensure your optimizations have real-world impact.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...