What does the render-blocking resources audit actually measure?
The audit flags two things: any stylesheet the browser must finish downloading before it can paint, and any script tag in the head that has neither defer nor async. That is the whole test. Everything I do to eliminate render-blocking resources WordPress plugins and themes pile into the head comes back to those two conditions.
Lighthouse checks each stylesheet link that is not disabled and has no media attribute mismatching the device, plus each script tag in the head without a defer or async attribute. Since Lighthouse 13 the standalone audit has moved into the “Render blocking requests” insight in both PageSpeed Insights and the DevTools Performance panel, and the wording now says these requests may delay LCP.
Why LCP? The largest element cannot paint before the first one does, and blocking CSS holds up first paint entirely. A head script without defer is worse: the HTML parser stops at that tag until the file downloads and runs. Cut both and First Contentful Paint moves earlier, which pulls LCP with it.
Finding the culprits before touching code
Start with the report itself. PageSpeed Insights lists each blocking URL with its size and estimated delay, worst first. On WordPress the usual suspects are a page builder stylesheet, a Google Fonts request, jQuery, and a slider or form plugin loading site-wide.
Chrome’s Coverage tab tells you how much of each file is dead weight. Open DevTools, press Ctrl+Shift+P (Cmd+Shift+P on Mac), type “coverage”, choose Show Coverage, then reload. Each CSS and JS file gets a bar: green is bytes that ran, gray is bytes that never did. A stylesheet showing 92 percent unused on your homepage is a dequeue candidate, not a defer candidate.
Query Monitor closes the loop by mapping URLs back to handles. Its Scripts and Styles panels show every enqueued handle, which plugin or theme registered it, its dependencies, and whether it prints in the head or footer. Those handle names are what every fix below needs.
Use the core defer and async strategy for your own scripts
WordPress 6.3 added a native strategy argument to wp_register_script() and wp_enqueue_script(), so you no longer need to hack the script_loader_tag filter. The fifth parameter that used to be a boolean $in_footer now also accepts an array.
Here is how I register a theme script that has no inline dependents:
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script(
'wpm-theme',
get_theme_file_uri( 'assets/js/theme.js' ),
array(),
'2.4.0',
array(
'strategy' => 'defer',
'in_footer' => false,
)
);
} );
With in_footer false the tag still prints in the head, but the defer attribute means the parser keeps going and the audit stops counting it. Reserve async for independent scripts such as analytics, because async scripts execute the moment they arrive, in no guaranteed order.
For a script that a plugin already registered, you can add the strategy after the fact without touching the plugin:
add_action( 'wp_enqueue_scripts', function () {
wp_script_add_data( 'some-plugin-handle', 'strategy', 'defer' );
}, 20 );
Two safeguards are built in. If a deferred script has a dependent registered as blocking, WordPress downgrades the deferred one to blocking so execution order holds. And if a handle has an inline script attached in the after position via wp_add_inline_script(), WordPress treats it as blocking and drops the strategy.
Eliminate render-blocking resources WordPress plugins add on pages that do not need them
The cleanest fix is not loading the file at all. Most plugins enqueue their CSS and JS on every request, so a contact form stylesheet blocks rendering on 200 blog posts that have no form. A conditional dequeue removes the request, the bytes, and the audit row in one move.
I put this kind of rule in a must-use plugin (wp-content/mu-plugins/) so it survives theme switches. Run it after the plugin has enqueued, which usually means priority 100 on wp_enqueue_scripts.
add_action( 'wp_enqueue_scripts', function () {
// Handles come from Query Monitor > Styles / Scripts.
if ( ! is_page( array( 'contact', 'request-a-quote' ) ) ) {
wp_dequeue_style( 'wpforms-full' );
wp_dequeue_script( 'wpforms' );
}
if ( ! is_front_page() ) {
wp_dequeue_style( 'slider-plugin-css' );
wp_dequeue_script( 'slider-plugin-js' );
}
}, 100 );
Handle names differ per plugin and per version, and a shortcode in a widget or footer can need the asset on pages you did not expect. After each dequeue, I load the affected templates with the console open and confirm nothing throws.
Should you inline critical CSS?
Yes, if your main stylesheet is large; otherwise it is optional. Inlining the styles needed for the first viewport lets the browser paint before the full stylesheet arrives, but Chrome’s own guidance calls it an advanced technique that most sites can skip while still hitting the recommended targets.
Extract the above-the-fold rules with a tool like Critical or Penthouse, inline them in a style block in the head, keep that block under roughly 14 KB compressed so it fits the first TCP round trip, and load the rest of the CSS without blocking:
add_filter( 'style_loader_tag', function ( $html, $handle, $href ) {
if ( 'wpm-main' !== $handle ) {
return $html;
}
return '<link rel="preload" as="style" href="' . esc_url( $href ) . '" '
. 'onload="this.onload=null;this.rel=\'stylesheet\'">'
. '<noscript><link rel="stylesheet" href="' . esc_url( $href ) . '"></noscript>';
}, 10, 3 );
FOUC is the tradeoff. Anything below the fold, or anything you missed in the extraction, renders bare for a moment until the full stylesheet arrives, and inlined CSS cannot be cached across pages, so every HTML response carries it. On a site with a 40 KB theme stylesheet the gain is small and the flash is real; on a builder site with 400 KB of CSS the math flips.
Google Fonts and font loading
A fonts.googleapis.com stylesheet in the head is render-blocking CSS plus two extra origins to connect to. Self-hosting removes both: download the WOFF2 files, declare them with @font-face and font-display: swap, and preload the one or two weights used above the fold. If you must stay on Google Fonts, add &display=swap to the URL and preconnect to fonts.gstatic.com.
Font swap can nudge layout, which is a different metric; the fix for that reflow lives in my walkthrough on fixing cumulative layout shift in WordPress. Here the goal is simply one fewer stylesheet request in the head.
Why jQuery in the head is the usual holdout
WordPress registers jQuery to print in the head, blocking, and it stays that way for a reason: thousands of plugins print inline jQuery(document).ready() snippets right after it. Defer jQuery and those snippets run before jQuery exists, so the console fills with jQuery is not defined and the slider, the mega menu, and the checkout quantity buttons all stop working.
You have three realistic options:
- Leave jQuery blocking and defer everything else. This is what I do on most client sites; one blocking script of roughly 30 KB compressed is a modest cost next to a broken checkout.
- Defer jQuery and every dependent together, then rewrite each inline snippet to wait for
DOMContentLoaded. Only worth it if you control all the code. - Use a delay-JS feature (WP Rocket, Perfmatters, FlyingPress) that holds all scripts until user interaction. It clears the audit but can make the first click feel dead, and it needs an exclusion list.
Do block themes have fewer render-blocking styles than classic themes?
Usually yes. Block themes turn on should_load_separate_core_block_assets by default, so WordPress prints styles only for the core blocks that appear on the page, each in its own inline style tag, instead of the combined wp-block-library stylesheet. Classic themes get the full stylesheet on every page.
Classic themes can opt in with one line:
add_filter( 'should_load_separate_core_block_assets', '__return_true' );
That only covers core block CSS. Plugin stylesheets, page builder CSS, and your theme’s main file still load the old way, so a block theme is a head start rather than a finish line. WordPress 7.0.4 on a block theme still fails the audit if a builder plugin injects 300 KB of CSS.
Which fix should you do first?
Order the work by gain per unit of risk. Deferring your own scripts and dequeuing unused plugin assets deliver most of the improvement with the least chance of breaking anything, so they go first. Critical CSS and delaying all JavaScript sit at the risky end and often become unnecessary once you eliminate render-blocking resources WordPress did not need to load in the first place.
| Technique | Difficulty | Risk of breakage | Typical gain |
|---|---|---|---|
Defer own scripts via strategy argument |
Low | Low | Removes 1-5 blocking scripts, 100-300 ms off FCP |
| Conditional dequeue of plugin CSS/JS | Low to medium | Medium (missed shortcodes) | Fewer requests site-wide, 100-500 ms |
| Self-host fonts, preload one weight | Low | Low | Removes 1-2 blocking stylesheets, cuts 2 origins |
| Enable separate core block styles (classic theme) | Low | Low | Drops the combined wp-block-library stylesheet on every page |
| Inline critical CSS, preload the rest | High | Medium (FOUC, missed rules) | Largest FCP gain on CSS-heavy sites |
| Defer or delay all JS including jQuery | Medium | High (inline scripts, forms) | Clears the audit, but breaks things without exclusions |
Those gains are ranges I see on typical shared and VPS hosting; yours depend on payload and server. Change one thing at a time and re-run the same PageSpeed test after each.
Plugins that do the work for you
If you do not control the theme, or you manage a fleet of sites, a plugin applies the same techniques through settings instead of PHP. Prices are the vendors’ listed annual rates as of August 2026.
| Plugin | Price | What it does for render-blocking |
|---|---|---|
| Autoptimize | Free (v3.1.15.1, 800k+ installs) | Aggregates and defers JS, inlines critical CSS, optimizes Google Fonts |
| Perfmatters | $29.95 (1 site), $59.95 (3), $124.95 (unlimited) | Script Manager for per-page dequeue, defer and delay JS with jQuery exclusion, local Google Fonts |
| WP Rocket | $59 (1 site), $119 (3), $299 (50) | Load JS deferred, delay JS execution, remove unused CSS, page caching |
| FlyingPress | $59 (1 site), $109 (3), $229 (25), $279 (unlimited) | Defer and delay JS, remove unused CSS, self-host Google Fonts, caching |
| Asset CleanUp | Free lite, paid Pro tier | Per-page unload of CSS/JS, defer JS in Pro |
My pick for a single site is Perfmatters, because the Script Manager is a visual version of the dequeue code above and it coexists with whatever cache layer the host provides. None of these replace the diagnosis step; a plugin with every box ticked and no exclusions is the fastest route to a broken site.
When deferring breaks something
The symptom is almost always in the console, so open it before you open a support ticket. Chrome DevTools > Console after a hard reload shows the exact error and the file that threw it, and it is nearly always one of these patterns.
Uncaught ReferenceError: jQuery is not defined or $ is not a function
An inline script ran before jQuery loaded. Either restore jQuery to blocking, or wrap the inline code so it waits:
document.addEventListener('DOMContentLoaded', function () {
jQuery(function ($) {
// original code here
});
});
A plugin’s function is undefined only on certain pages
You dequeued its asset on a template that still uses it, often through a shortcode in a widget, footer, or reusable block. Add that template to the conditional or scope the rule with has_shortcode().
Slider or menu is visible but does not respond
Delay-JS is holding the script until interaction. Exclude that plugin’s handle from delay and leave it on defer instead.
Styles flash unstyled then snap in
Critical CSS is missing rules for that template. Regenerate it per template type, or drop inlining there and rely on defer plus dequeue.
Nothing changed after adding strategy
Query Monitor will show an inline after-script on that handle, or a blocking dependent. Move the snippet to the before position, or defer the dependent too. And purge page cache; every fix in this post is invisible until the cached HTML is regenerated.
Keep a note of every dequeue and defer rule you add. Plugin updates rename handles and add new inline snippets, which is why re-running the audit belongs on the monthly WordPress maintenance checklist rather than being a one-time task.
Your first move today: run your homepage through PageSpeed Insights, copy the top three URLs from the render-blocking list, and match them to handles in Query Monitor. That one step tells you whether the fix is a two-line strategy change, a conditional dequeue, or something a WordPress speed optimization engagement should profile properly.
Frequently Asked Questions
Yes, mainly LCP. Render-blocking CSS and head scripts delay the first paint, and the largest element cannot paint before the first one does. Clearing them usually moves LCP by a few hundred milliseconds on a typical WordPress site. It has little direct effect on CLS or INP.
No. Deferring jQuery or any script that an inline script depends on will throw errors like jQuery is not defined and break sliders, forms, and menus. Defer scripts individually, keep jQuery and its dependents in the same mode, and test the site with the browser console open after each change.
Not necessarily. Since WordPress 6.3 you can defer or async your own scripts with a single argument in wp_enqueue_script, and you can dequeue plugin assets in a small mu-plugin. A plugin like Perfmatters or WP Rocket saves time when you do not control the theme code or want per-page controls without writing PHP.
Still failing the render-blocking audit after the easy fixes?
We profile the critical rendering path, defer what is safe, drop what is unused, and hand you before-and-after Lighthouse traces.