Generate Critical CSS and Load Full Stylesheets Asynchronously in WordPress

Critical CSS is the minimal set of above-the-fold styles required to render the visible portion of a page without blocking the browser on the full stylesheet download — inlining it in a <style> block in <head> eliminates the render-blocking stylesheet request and directly improves Largest Contentful Paint (LCP). Google PageSpeed Insights reports “Eliminate render-blocking resources” as one of the highest-impact recommendations for WordPress sites, and critical CSS inlining is the primary technique for addressing it on CSS-heavy themes. Generating critical CSS manually is impractical — the critical npm package uses Puppeteer to load each page type, extract the above-the-fold styles, and output the result as a string suitable for inlining. The full stylesheet is then loaded asynchronously using the rel="preload" + onload pattern, ensuring it applies before the user scrolls without blocking the initial render. WordPress’s wp_add_inline_style() inlines CSS after a given stylesheet handle, but for critical CSS the better approach is to output the <style> block directly in wp_head at priority 1, before any enqueued stylesheets. Cache the per-page critical CSS string in a transient keyed by the page URL — regenerating it on every request defeats the performance purpose entirely. Separate critical CSS sets for homepage, single post, archive, and WooCommerce shop improve accuracy compared to a single global critical CSS file that must cover all page types. The font-display: swap CSS property for Google Fonts prevents invisible text during font loading and complements critical CSS by ensuring layout-relevant font metrics are available from the first render cycle. The defer JS post applies the same render-blocking elimination principle to scripts that critical CSS applies to stylesheets — combine both for maximum LCP improvement. The browser caching post ensures the full stylesheet, once loaded asynchronously, is cached in the browser and not re-downloaded on subsequent page views. Re-generate critical CSS after every major theme update — theme changes alter the above-the-fold selector set and stale critical CSS causes layout shifts visible during the first render.

Problem: WordPress themes load full stylesheets synchronously in <head>, blocking the browser from rendering any content until the entire CSS file is downloaded and parsed — causing poor Largest Contentful Paint scores on mobile connections and triggering PageSpeed "render-blocking resources" warnings.

Solution: Extract above-the-fold critical CSS using the critical npm package, inline it via wp_head at high priority, and load the full stylesheet asynchronously with rel="preload" and a JavaScript onload fallback.

// 1. Generate critical CSS (run in theme directory)
// npm install --save-dev critical
// node generate-critical.js

// generate-critical.js
const critical = require('critical');
const pages = [
    { url: 'https://helloadmin.com/',         out: 'critical-home.css' },
    { url: 'https://helloadmin.com/?p=1322',  out: 'critical-single.css' },
];
pages.forEach(({ url, out }) => {
    critical.generate({
        src:    url,
        width:  1300,
        height: 900,
        dest:   'assets/critical/' + out,
        inline: false,
        extract: false,
        penthouse: { timeout: 30000 },
    }).then(() => console.log('Generated:', out));
});

// 2. Inline critical CSS and async-load full stylesheet in functions.php

add_action('wp_head', function() {
    $template = is_singular() ? 'single' : 'home';
    $file     = get_theme_file_path("assets/critical/critical-{$template}.css");
    if (!file_exists($file)) return;

    $cache_key = 'critical_css_' . $template;
    $css = get_transient($cache_key);
    if (!$css) {
        $css = file_get_contents($file);
        set_transient($cache_key, $css, WEEK_IN_SECONDS);
    }
    echo '<style id="critical-css">' . $css . '</style>' . "
";
}, 1);

// Dequeue blocking stylesheet and reload it asynchronously
add_filter('style_loader_tag', function(string $tag, string $handle): string {
    if ($handle !== 'mytheme-style') return $tag;
    // Convert blocking link to preload + noscript fallback
    $preload  = str_replace("rel='stylesheet'", "rel='preload' as='style' onload="this.onload=null;this.rel='stylesheet'"", $tag);
    $noscript = '<noscript>' . $tag . '</noscript>';
    return $preload . $noscript;
}, 10, 2);

NOTE: The onload="this.rel='stylesheet'" pattern requires JavaScript — always include the <noscript> fallback so the full stylesheet loads for users with JavaScript disabled, including some crawlers and accessibility tools that run in no-script mode.