Reduce Unused JavaScript and CSS in WordPress
The PageSpeed warnings about unused JavaScript, unused CSS, cache policy and script execution each have a WordPress-specific cause, a code fix, a plugin fix and a way to break the site.
Some links on this page are affiliate links: if you buy through one, we may earn a commission at no extra cost to you. It does not change what we recommend; see the editorial policy.
Each PageSpeed warning names a symptom, not a fix. On WordPress the cause is almost always the same: plugins and themes load their assets on every page, whether or not the page uses them. Fix that first, then tune what remains.
This page covers the five warnings that follow from that habit. Render-blocking resources, critical CSS and deferring scripts have their own guide.
The warnings at a glance
| Warning | WordPress cause | First fix | Risk |
|---|---|---|---|
| Reduce unused JavaScript | Plugin scripts loaded site-wide | Dequeue per page | Low, if handles are right |
| Reduce unused CSS | Theme and plugin stylesheets everywhere | Dequeue, then trim | High with automatic removal |
| Serve static assets with an efficient cache policy | No expiry headers on the server | .htaccess or nginx rules |
Low |
| Reduce JavaScript execution time / Minimize main-thread work | Third-party tags running at load | Delay until interaction | Medium: breaks widgets |
| Minify CSS and JavaScript | Unminified plugin files | Plugin or build step | Low |
Reduce unused JavaScript
What it measures: bytes downloaded in script files that did not execute during page load. A slider library on a page with no slider is all unused.
The WordPress cause: most plugins call wp_enqueue_script on every front-end request because they cannot know where you placed their shortcode or widget. Form, slider, gallery and booking plugins are the usual offenders, and a page builder adds its own frontend bundle on top.
Fix without a plugin: dequeue conditionally from a site-specific plugin, after the plugin has enqueued:
<?php
add_action('wp_enqueue_scripts', function () {
// WooCommerce styles only where the shop actually renders.
if (function_exists('is_woocommerce') && ! is_woocommerce() && ! is_cart() && ! is_checkout() && ! is_account_page()) {
wp_dequeue_style('woocommerce-general');
wp_dequeue_style('woocommerce-layout');
wp_dequeue_style('woocommerce-smallscreen');
}
if (! is_front_page()) {
wp_dequeue_script('home-slider');
}
}, 100);
Find the handles in the plugin's enqueue calls; the hook and priority rules are in wp_enqueue_scripts. If another enqueued script lists the handle as a dependency, WordPress loads it anyway and the dequeue does nothing — check the Network tab. Deregistering it instead would also drop the script that depends on it.
Fix with a plugin: asset managers give a per-page checkbox list of every script and stylesheet. Same result, easier to maintain on a site with many page types.
The risk: a page that uses the feature through a widget or a reusable block you forgot. Test every template, not every page.
Reduce unused CSS
What it measures: CSS rules in downloaded stylesheets that matched nothing on the page during load.
The WordPress cause: a theme ships one stylesheet for every layout it supports, and each plugin adds its own. A blog post can easily download styles for WooCommerce, forms, sliders and the mega menu.
Fix without a plugin: dequeue whole plugin stylesheets where they are not used, as above. For the theme, a lean theme or a block theme loads styles per block, which solves most of it at the source.
Fix with a plugin: "remove unused CSS" features render each page, record which selectors matched, and serve only those. This is the biggest single gain in the report and the most dangerous one.
The risk: the snapshot is taken from one state of the page. Hover states, open mobile menus, form validation messages, logged-in views and anything injected by JavaScript later were not in that state, so their rules disappear. Every broken dropdown after enabling it traces back here. Add safelists for those selectors, or keep the feature off and settle for dequeuing.
Serve static assets with an efficient cache policy
Newer reports call it "Use efficient cache lifetimes".
What it measures: images, CSS, JavaScript and fonts served with a short or missing Cache-Control lifetime, so returning visitors download them again.
The WordPress cause: WordPress sends no headers for static files at all; the web server does. Many shared hosts ship without expiry rules. WordPress appends ?ver= to enqueued assets, so long lifetimes are safe: a new version gets a new URL, as long as the plugin or theme bumps its version. Images and fonts referenced from CSS get no ?ver=, so replace them under a new filename.
Fix on Apache, in .htaccess above the WordPress block:
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/webp "access plus 1 year"
ExpiresByType image/avif "access plus 1 year"
ExpiresByType image/jpeg "access plus 1 year"
ExpiresByType image/png "access plus 1 year"
ExpiresByType image/svg+xml "access plus 1 year"
ExpiresByType font/woff2 "access plus 1 year"
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
ExpiresByType text/javascript "access plus 1 year"
</IfModule>
Fix on nginx, inside the server block:
location ~* \.(?:css|js|webp|avif|jpe?g|png|gif|svg|ico|woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
access_log off;
}
An add_header inside a location replaces the server-level ones for these files, so repeat your security headers inside this block.
Cache plugins write the Apache rules for you on activation, and a CDN can set them at the edge, per Cloudflare and WordPress.
What you cannot fix: files from other domains — analytics, tag managers, maps, chat, ad scripts. Their owners keep lifetimes short deliberately. They stay in the list, and that is fine.
Delay JavaScript execution
What it measures: main-thread time spent parsing and running scripts during load, reported as "Reduce JavaScript execution time" and "Minimize main-thread work".
The WordPress cause: third-party tags. A consent banner, analytics, a pixel or two and a chat widget routinely cost more main-thread time than the theme and every plugin combined.
Fix without a plugin: remove tags nobody reads the data from, load what remains async, and defer what remains where the vendor allows it — for enqueued scripts, pass 'strategy' => 'defer' to wp_enqueue_script (WordPress 6.3+); script_loader_tag is for markup you do not control.
Fix with a plugin: "delay JavaScript until interaction" holds selected scripts back until the first scroll, tap or keypress. It is the setting that moves scores most, and performance plugins such as WP Rocket ship it with an exclusion list already filled in for common sliders and builders.
The risk: anything visible that a script builds — sliders, galleries, maps, cookie banners — stays empty until the visitor interacts. Delay tracking and chat; exclude everything that renders content.
Minify CSS and JavaScript
Minification strips whitespace and comments. Gains are small next to the warnings above, and the risk is low as long as files are only minified, not combined. Without a plugin, it means a build step in your own theme; plugin files you can only minify with a plugin or at the CDN. Combining is not worth it under HTTP/2; see the render-blocking guide.
Common mistakes
- Enabling every toggle at once, then not knowing which one broke checkout.
- Testing logged in. Admins skip the cache and load the admin bar's assets.
- Chasing third-party entries in the cache policy list.
- Dequeuing a handle that is a dependency, which does nothing (the dependent pulls it back in), or deregistering it, which silently drops the dependent.
- Removing unused CSS on a WooCommerce site without checking cart, checkout and account pages.
Verify
- Run the report on the homepage, one post and one template-heavy page, logged out.
- In DevTools, confirm dequeued files no longer appear in the Network tab on pages that do not use them.
- Check a static file's response for
Cache-Control: max-age=31536000, as shown in how to check HTTP headers. - Click through menus, forms, sliders and the cart on mobile.
- Compare field data in Search Console after a few weeks, as described in speeding up a WordPress site.
Frequently asked
- Rarely, and it is not worth trying. Analytics, consent banners and the theme's own bundle always contain code a given page does not run. Aim to remove whole files that a page does not need, then stop.
- Third-party scripts such as analytics, tag managers, maps and chat widgets are served from their own domains with short expiry on purpose, so updates reach every site quickly. Your server headers do not apply to them, so leave them on the list.
- Delaying analytics, chat and pixels until interaction does not affect how the page is indexed. Delaying scripts that build visible content, like a slider or a product gallery, can leave crawlers and visitors looking at an empty block, so exclude those.