Features

File Optimization

File optimization minifies, combines, and defers CSS and JavaScript to reduce render-blocking resources and improve load times. These settings have the most dramatic impact on Core Web Vitals scores, particularly Largest Contentful Paint (LCP) and Interaction to Next Paint (INP).

  • Covers Caching Out 1.25.0
  • Last reviewed 2026-10-30

Minification

Minification removes unnecessary characters from CSS and JavaScript files — whitespace, comments, line breaks, and block delimiters — without changing functionality. The result is functionally identical code in a smaller file.

The two switches alone decide. Caching Out does not consult WordPress's SCRIPT_DEBUG constant: a development site that defines it still serves minified copies while Minify CSS or Minify JS is on. Switch the relevant option off while you debug a stylesheet or script, or add that one file to the exclusion list. (Minified copies are written next to the cache, the original files are never touched, and a file that is already minified is served as it is.)

Minify CSS

Removes whitespace, comments, and unnecessary semicolons from CSS files. Typically reduces CSS file size by 20-30%.

Minify JS

Removes whitespace, comments, and shortens variable names where safe. Typically reduces JS file size by 30-50%.

Combining

Combining merges multiple CSS or JavaScript files into fewer, larger files. This reduces the number of HTTP requests the browser must make.

Important: Combining is off by default and should be used selectively:

  • HTTP/1.1: Combining is beneficial because browsers are limited to ~6 concurrent connections per domain. Fewer files = faster loading.
  • HTTP/2+: Combining can be counterproductive. HTTP/2 multiplexes many requests over a single connection, so many small files load just as fast as one large file. Combining also breaks per-file caching — changing one small file forces re-download of the entire combined bundle.

Combine CSS and Optimize CSS Delivery together (changed in 1.21.5). Optimize CSS Delivery keeps the active theme's stylesheets render-blocking (see "Theme stylesheet and coverage guard"), and a combined file that contains a theme stylesheet inherits that protection. Before 1.21.5 Combine CSS put every consecutive local stylesheet into ONE file, so a theme stylesheet next to a grid framework, a slider plugin's CSS and a consent banner's CSS dragged all of them into a single render-blocking request, most of it unused on the page. From 1.21.5 Combine CSS builds separate files for protected and deferrable stylesheets: consecutive theme stylesheets go into one file, the consecutive plugin stylesheets after them into another, and Optimize CSS Delivery defers the second. Stylesheets are never reordered to do this: only neighbours of the same kind are combined, and a bundle's contents follow the page order, so the cascade is exactly what the theme and plugins enqueued. A theme stylesheet sitting between two plugin stylesheets therefore combines with neither. Developers can restore a single bundle with add_filter( 'cachingout_combine_css_split', '__return_false' ).

Optimize CSS Delivery

This is one of the most impactful settings for improving LCP. Render-blocking CSS prevents the browser from displaying any content until all CSS files are downloaded and parsed.

The plugin:

  1. Identifies the critical CSS needed for above-the-fold content
  2. Inlines that critical CSS directly in the <head>
  3. Loads the remaining CSS asynchronously

The result: the browser can render visible content immediately while non-critical styles load in the background.

Stylesheets that lay out the page (kept render-blocking automatically)

Loading a stylesheet asynchronously is safe when it only styles what is on the page. It is not safe when the stylesheet lays out markup that is already in the HTML. The classic case is a carousel.

A slider's markup is a wrapper with one child per slide. The library's stylesheet is what turns that wrapper into a single-row flex container. Until the stylesheet applies, the slides are ordinary blocks, so they stack vertically at full width. Five 700×700 slides on a phone occupy roughly 2,000px. The moment the stylesheet arrives, the wrapper collapses to the height of one slide and everything below it jumps up the page.

That is a large Cumulative Layout Shift, and it has two properties that make it hard to recognise:

  • A reserved height does not fix it. Setting min-height on the carousel assumes the box is too short before it initialises. It is too tall. The reservation changes nothing.
  • It does not reproduce reliably. Slide images are usually lazy-loaded, so on a fast connection or a cold cache they may never arrive inside the measurement window. The same page can score 63 on one Lighthouse run and 44 on the next, with no change in between. Always take at least two runs.

The plugin handles this for you. Optimize CSS Delivery keeps the common carousel and slider stylesheets render-blocking, out of the box, with no configuration: Swiper, Slick, Owl Carousel, Flickity, Splide, Glide, Tiny Slider, FlexSlider, Light Slider, Keen Slider, Royal Slider and Nivo Slider. Combine CSS also leaves them out of the bundle, because a stylesheet merged into a combined file loses its name and could no longer be recognised.

If your slider is not on that list, add a fragment of its file name (for example my-carousel) to the Excluded CSS list. That list protects a stylesheet from minification, from combining, and from being loaded asynchronously.

Developers can override the decision per stylesheet with the cachingout_critical_css_skip_link filter:

add_filter( 'cachingout_critical_css_skip_link', function ( $skip, $href ) {
	if ( false !== strpos( $href, 'my-carousel' ) ) {
		return true; // Keep it render-blocking.
	}
	return $skip;
}, 10, 2 );

This is the CSS counterpart of the Delay JS dependency problem described under "Libraries that other code depends on" below. Same underlying mistake: an optimization deferred something that other page content depended on being there.

Theme stylesheet and coverage guard

Sliders are the loud case; the quiet one is the theme itself. On a real site the theme's main stylesheet held the only rules that lay out the top of the home page (a promo bar, a hero block), the heuristic critical CSS had none of them, and the page rendered half-styled and then reflowed when the deferred stylesheet applied: a Cumulative Layout Shift of about 1.0 and a mobile PageSpeed score in the 60s. With the feature off the same page scored in the 90s. Two rules now prevent that, with no configuration:

  • The active theme's stylesheets are never deferred. Every stylesheet served from the parent or child theme directory (style.css, a built dist/main.css, a theme's reset.css) stays render-blocking. A Combine CSS bundle that contains one of them inherits the protection; the plugin remembers which files went into each bundle. The WordPress block library stylesheet that classic themes load (wp-includes/css/dist/block-library/style.css) is on the built-in list too: it lays out every block on the page.
  • Coverage guard. Before deferring any other local stylesheet the plugin reads the names of the elements at the top of the page (the header, navigation, any promo or announcement bar, and the first block of the main content), finds the rules in that stylesheet that lay those elements out (display, position, size, spacing, font size and the like; colour-only rules do not count), and checks that the inline critical CSS carries them too. If any are missing the stylesheet stays render-blocking. The critical CSS itself is now built page by page from those same names first, so on most pages everything is covered and every stylesheet is still deferred.

Rules inside media queries are handled the same way since 1.21.4: a mobile-only layout rule for a visible element is inlined inside its @media block (and its @supports condition), never as an unconditional rule, and it counts for coverage like any other layout rule.

Both rules apply to stylesheets the plugin can read from your own server. A stylesheet loaded from another host (a CDN, a font service) cannot be read, so it is deferred exactly as before; if such a file lays out your page, add a fragment of its name to Excluded CSS. A stylesheet the plugin cannot parse (over 400 KB) is kept render-blocking to be safe.

Minify CSS and the protections (fixed in 1.21.3)

Before 1.21.3, Minify CSS defeated every name- and location-based protection. Minify CSS runs before Optimize CSS Delivery and rewrites every local stylesheet to a hashed copy under wp-content/cache/cachingout/min/css/. The protections recognise a stylesheet by its name (the built-in slider list, your Excluded CSS) or by its location (the theme directory), and until 1.21.3 they saw only the hashed name, so with both features on the theme's own stylesheet was deferred and the header could render as a vertical list of links, lose a button, or shift, until the stylesheet arrived. From 1.21.3 the plugin remembers where every minified copy came from (the same way it already did for combined files), so the theme, slider and excluded stylesheets keep their protection with Minify CSS on. If you still see the symptom on 1.21.3 or later, save any setting once so the minified copies are regenerated with their record, purge, and check the debug header for :theme on the theme stylesheet.

Measure before you keep it on. Optimize CSS Delivery is off by default and is a tuning step, not a default. When the theme ships one large stylesheet bundle, which is the common case, the protection keeps that bundle render-blocking, so the feature pays for its inline critical block on every page and only defers the small remaining sheets (consent banners, plugin styles). On such a site it can lower the PageSpeed score rather than raise it, even with the protection working as designed. Run PageSpeed Insights twice with the feature off and twice with it on, after warming the cache, and keep whichever configuration scores higher. Minify CSS alone is the safe gain on most sites.

Debug header, Page Audit findings and the one-click fix

You can see what the feature decided in three places:

  • Page Audit (Insights tab). Layout stylesheets kept (informational) lists the stylesheets left render-blocking on the sampled page and why: theme stylesheet, on the built-in layout-critical list, listed in Excluded CSS, lays out visible elements the inline CSS does not cover, or could not be parsed. Layout stylesheet deferred (high) is the one to act on: a deferred stylesheet still lays out the top of that page (the selectors are listed). It should not normally appear; when it does, the Keep it render-blocking button adds the file name to Excluded CSS for you. The change is saved like any other setting, appears on the Activity page with source page_audit, and can be reverted there. Purge the cache afterwards and re-run the audit.
  • Filesystem Check row Optimize CSS delivery: what the front page's last uncached render kept render-blocking.
  • Debug header (with CACHINGOUT_DEBUG on): X-CachingOut-CSS: deferred=4; kept=style.css:theme; kept=main.css:uncovered:.hp-hero,.hp-promo-bar. deferred counts the stylesheets loaded asynchronously; each kept= names a file, the reason (theme, builtin, excluded, uncovered, parse) and, for uncovered, up to six of the selectors involved.

Developers can override either rule:

// Let the theme stylesheet be deferred after all (the coverage guard still applies).
add_filter( 'cachingout_critical_css_theme_protected', function ( $protected, $href ) {
	return false !== strpos( $href, '/themes/my-theme/style.css' ) ? false : $protected;
}, 10, 2 );

// Switch the coverage guard off entirely (pre-0.1.21 behaviour: defer everything not excluded).
add_filter( 'cachingout_critical_css_coverage_guard', '__return_false' );

Defer JavaScript

Adds the defer attribute to script tags, telling the browser to download the script in parallel with HTML parsing and execute it only after the page has finished parsing. This prevents JavaScript from blocking page rendering.

Defer JavaScript never defers jQuery or jQuery Migrate. Almost every inline script on a WordPress page expects jQuery (and $) to exist the moment it runs, and a deferred jQuery would arrive after them, so those two files are left exactly where WordPress prints them. With Defer JavaScript on they are therefore usually the only render-blocking scripts left in the head, about 38 KB on the wire. The setting below is the safe answer to that.

Load jQuery in the footer (File Optimization, JavaScript loading) asks WordPress to print jQuery, jQuery Migrate and every script attached to them at the end of the page instead of in the head. jQuery still runs synchronously, before every script that depends on it, so inline jQuery code that WordPress places after it keeps working; only the position moves, and the browser reaches its first paint without waiting for it. Off by default. Designed to remove the last render-blocking scripts from the head on a site that already uses Defer JavaScript.

Two things to know:

  • WordPress can refuse, and then nothing changes. When any script printed in the head depends on jQuery, WordPress keeps jQuery in the head so that script still works. The setting then has no effect and no cost. The Filesystem Check row Load jQuery in the footer names the head scripts responsible, for example theme-head or some-plugin-js; ask that theme or plugin to load its script in the footer, or accept the head position. With CACHINGOUT_DEBUG on, the response carries X-CachingOut-jQuery: footer when it moved and X-CachingOut-jQuery: head:pinned-by=theme-head when it did not.
  • A script hard-coded into a header template can break. A theme that writes <script>jQuery(function($){ ... })</script> straight into header.php, rather than attaching it to the jquery handle, now runs before jQuery exists and throws ReferenceError: jQuery is not defined (or $ is not defined) in the browser console. Switch the setting off, move that snippet to the footer template, or attach it with wp_add_inline_script( 'jquery', ... ) so it travels with jQuery.

Developers: the cachingout_jquery_footer_handles filter adds handles to move (a theme's own jQuery build registered under another name); returning false from cachingout_jquery_footer_applies skips a request. The setting is honoured nowhere in wp-admin, the customizer preview, AJAX, REST or cron, and CACHINGOUT_DISABLE_OPTIMIZATION switches it off with the rest of the pipeline.

Defer jQuery, opt-in (Pro)

Defer jQuery (I have checked my inline scripts) is the full lever. With it on, and Defer JavaScript on, jQuery and jQuery Migrate get the defer attribute like every other script, so no script blocks the first paint at all. It is off by default and the label carries a warning, because it is only safe when nothing on the page uses jQuery before the page has finished loading.

The plugin helps you check. On every uncached render it scans the page for inline <script> blocks that reference jQuery, $( or $. and records their ids: WordPress prints an inline script attached to a handle as id="<handle>-js-after" (or -js-before), so the id names the script that owns it. The Filesystem Check row Defer jQuery (opt-in) lists what it found on the front page; with CACHINGOUT_DEBUG on, the response carries X-CachingOut-jQuery-Defer: deferred=2; inline-refs=1 (theme-js-after). Switch the setting on only when that list is empty, or when you have dealt with every entry:

  • Wrap the snippet in document.addEventListener('DOMContentLoaded', function () { ... }). Deferred scripts always run before that event, so jQuery is ready inside it.
  • Or leave the setting off and use Load jQuery in the footer instead, which never breaks an inline script that WordPress prints after jQuery.

Notes: the user's Excluded JS list still wins (add jquery there to keep jQuery blocking with everything else deferred); with Combine JS on, jQuery is inside a combined bundle that Defer JavaScript already defers, so this setting finds nothing to do; Delay JavaScript never delays jQuery either way. Developers can extend the URL fragments the setting recognises with the cachingout_jquery_defer_patterns filter.

Delay JavaScript

Holds JavaScript execution until the user interacts with the page (click, scroll, or mouse movement) or a configurable timeout is reached. This is designed to improve Interaction to Next Paint (INP) by preventing JavaScript from competing with user interactions during initial page load.

Delay Timeout

Maximum time to wait for user interaction before executing delayed scripts anyway. Default: 10 seconds.

Exclusion List

Scripts that must run immediately, regardless of user interaction. Common exclusions:

  • Consent/cookie banners (must appear before interaction). Built in since 1.21.15: CookieYes (hosted and the self-hosted cookie-law-info plugin), Cookiebot, Complianz, FAZ Cookie Manager, Borlabs Cookie, iubenda, Real Cookie Banner, Cookie Notice, Termly, Osano, Usercentrics and cookie-script are never delayed, and the same families keep their stylesheets render-blocking under Optimize CSS Delivery. Any other consent plugin: add its directory name here.
  • Live chat widgets (must initialize early)
  • Analytics/tracking scripts (must capture page view)
  • A/B testing scripts (must apply variant before render)

Enter script keywords, URLs, or file names — one per line. Each line is matched as plain text, case-insensitively, anywhere in the script's URL or in its id attribute (WordPress writes <handle>-js); it is not a regular expression and * is not a wildcard. Use the shortest distinctive fragment, swiper rather than swiper.min.js: a site with SCRIPT_DEBUG on serves swiper.js, and when Minify JS has rewritten the URL to a hashed filename only the id still names the script (from 1.21.6; before that a minified script could not be excluded by name). Inline scripts have no URL and are never delayed.

Google Analytics and Google Tag Manager scripts are not delayed by default, so every page view is recorded. The opt-in Also delay analytics and tag-manager scripts delays them like any other script: they load after the first tap, scroll or key press, or at the timeout. What it buys is lab time. PageSpeed Insights and Lighthouse never interact with the page, so in the lab the tag loads only at the timeout and its main-thread work (typically 150 to 300 ms for gtag.js) leaves the measured window, which is a large part of the score advantage some optimizers advertise. What it costs is real visits shorter than the delay timeout, which go unrecorded. Turn it on when the score matters more than counting the shortest visits, and keep the timeout short.

Two things the setting cannot reach, by design:

  • Inline loader snippets. A gtag() or fbq() bootstrap written inline into the page (no src) is never delayed: inline code is left alone. Only the external script it loads can be delayed.
  • Consent-managed placeholders. With a consent plugin such as Complianz in "consent mode", the analytics tag is printed as a type="text/plain" data-cmplz-src="..." placeholder that the consent plugin itself activates at load with denied signals. That tag is not a script, so Delay JavaScript does not touch it (touching it broke Complianz in an earlier version), and the consent plugin loads the tag on every visit, consent or not. If you want analytics held back on such a site, use the consent plugin's own "block until consent" mode instead of consent mode; the plugin cannot make that choice for you.

Libraries that other code depends on (must be excluded)

Delay JS holds back a script until the first tap, scroll or mouse move. That is safe for self-contained scripts. It is not safe for a library that other, non-delayed code calls at page load. Typical case: a theme loads a carousel library (Swiper, Splide, Slick, Owl Carousel, Flickity) as a normal <script src> tag and then initialises it from an inline <script> on DOMContentLoaded. The library is delayed; the inline initialiser is not (inline code with no src is left alone). The initialiser runs, finds no Swiper global, throws ReferenceError: Swiper is not defined, and the carousel is never created. The symptoms look like a broken theme, not like a caching problem:

  • A slider or carousel only nudges a few pixels when swiped on a phone, and a further swipe or tap opens the link under the slide (the browser sees a plain click on an <a>).
  • A hero slider shows only the first slide; arrows and dots do nothing.
  • Anything initialised on DOMContentLoaded that depends on a delayed library: lightboxes (GLightbox, Fancybox, Magnific Popup), animation libraries (GSAP, AOS, Lottie), grids (Isotope, Masonry), maps (Leaflet, Mapbox), charts (Chart.js).
  • Because the failing call throws, everything written after it in the same listener also stops running, so unrelated features can break alongside the carousel.

The delayed library does load after the first interaction, and Delay JS replays DOMContentLoaded for the delayed scripts themselves, but it cannot re-run the theme's inline listener that already ran and failed. So the feature stays broken for the whole visit.

How to confirm it: open the browser console on the page; look for ReferenceError: <Library> is not defined at page load. In the page source, the library's <script> tag will carry type="cachingoutlazyloadscript" with its URL in data-cachingout-src, while the inline initialiser calls new <Library>( unchanged.

Fix: add the library's name (for example swiper) to the Delayed JS exclusions list. The library then loads normally, in its original position, and the inline initialiser finds it. Saving the setting purges the page cache. The theme-side hardening is to guard the initialiser with if (typeof Swiper === 'undefined') return; or to initialise inside the library's own load event, but the exclusion alone restores the feature.

Built-in exclusions already cover jQuery and jQuery Migrate for exactly this reason: almost every inline script depends on jQuery being present at load. From 1.21.6 they also cover WordPress's own wp-polyfill, wp-hooks, wp-i18n, wp-dom-ready, wp-url and wp-api-fetch: WordPress prints wp.i18n.setLocaleData(...) as an inline script straight after wp-i18n, so delaying that library produced wp is not defined and broke every translated plugin script (Contact Form 7 among them). The patterns carry no .min, so they hold on a SCRIPT_DEBUG site too.

Google Fonts (Pro)

Optimize Google Fonts merges every Google Fonts request on a page into one, adds display=swap so text is visible in a fallback font while the web font loads, and preconnects to the font hosts. Self-host fonts goes further: the plugin fetches the font CSS as a current browser would (WOFF2 files with font-display), downloads the font files into wp-content/cache/cachingout/fonts/, rewrites the CSS to point at them, and inlines it in the head, so no request leaves your domain for fonts. The CSS is refreshed weekly.

Subsets follow your site's languages (1.21.5). Google serves one @font-face block per unicode-range subset it has for a family: for Lato that is six subsets (latin, latin-ext, cyrillic, cyrillic-ext, greek, greek-ext), so ten faces became sixty blocks and 26 KB of inline CSS, of which a Polish site needs latin and latin-ext only, and Lighthouse counted the rest as unused CSS. The plugin now reads the site language (and every Polylang or WPML language) and keeps only the subsets those languages need: Western European languages need latin alone; Polish, Czech, Hungarian, Romanian, Turkish, Croatian and the Baltic languages add latin-ext; Russian, Ukrainian and Bulgarian add cyrillic; Greek, Vietnamese, Hebrew, Arabic and the Indic languages add their own. Dropped blocks are never downloaded. A language the plugin does not know, and Japanese, Korean or Chinese (served as many small slices), keep every subset, so nothing is ever guessed away. With CACHINGOUT_DEBUG on, the response carries X-CachingOut-Fonts: subsets=latin,latin-ext; blocks=10; dropped=50. Developers can change the list with the cachingout_font_subsets filter (return an array of subset names, or null for all) or the locales it is derived from with cachingout_font_locales.

Exclusion Controls

Every optimization feature has per-file exclusion controls. If a specific CSS or JS file breaks when minified, combined, or deferred, add it to the exclusion list for that feature.

Logged-in views run the optimizations too. Page caching never serves a logged-in user a cached copy, but Minify, Combine, Defer and the other file optimizations are applied to every front-end render, including the ones you see while logged in with the admin bar showing. So with Minify CSS on, the admin bar's own stylesheets (dashicons, admin-bar.css, some plugins' admin-bar styles) get minified copies under wp-content/cache/cachingout/min/ although no anonymous visitor ever loads them. That is expected and harmless: a few extra small files that the purge age-out removes when idle. It also means that what you see logged in is a fair preview of the optimized page, minus the cache itself.

Excluded CSS / JS

Skip specific files during minification and combining. Enter file names or URL fragments, one per line.

Excluded Inline CSS / JS

Skip inline <style> and <script> blocks during minification. Enter identifying strings from the inline content, one per line.

Unused CSS/JS

Per-page asset unloading: stop a plugin's stylesheet or script loading on pages that do not use it.

WordPress plugins load their CSS and JavaScript on every page by default: a contact-form stylesheet ships on every page of a site with one contact page, and WooCommerce block styles ship on a homepage that sells nothing. Minify, Combine and Optimize CSS Delivery make those files smaller, fewer and later. The Unused CSS/JS tab (under File Optimization) makes them absent on the pages that do not use them. Stops plugin CSS and JavaScript from loading on pages that do not use them; typically removes render-blocking requests from pages where a plugin's features are not present.

Nothing is unloaded until you add a rule and switch it on. This is the one optimization that removes content, so it is never enabled by default.

How a rule works

A rule names one file by its WordPress handle (the name the plugin registered, for example wc-blocks-style or contact-form-7), a scope, and the pages the scope refers to:

  • Everywhere except the pages below unloads the file on every page except the ones the matchers select. This covers the usual case: "WooCommerce block styles off every non-shop page".
  • Only on the pages below unloads it only there: "this slider stylesheet is needed nowhere but the front page" reads better the other way round, so this scope is rarer.

Matchers: front page; archives, search and 404; post types; page IDs; page slugs; URL prefixes; pages containing a shortcode; pages containing a block; WooCommerce pages (shop, cart, checkout, account, product). Any matcher hit selects the page. Shortcode and block matching works on single posts and pages only; archives are never scanned for them.

The rule takes effect through WordPress's own enqueue system, so the file's dependency information is available: a file that another loaded file depends on is kept, and the reason is shown in the preview and the debug header. Switching off "Refuse unsafe rules" only lifts that refusal; WordPress still prints a file its dependants require.

What can never be unloaded

jQuery, the active theme's stylesheet, block styles on pages that contain blocks, the admin bar, consent, payment and reCAPTCHA scripts, and this plugin's own assets. A rule naming one of them is dropped on save. Developers can extend that list with the cachingout_asset_unload_never filter; it cannot be shortened.

Complianz (consent banner) is deliberately not on that list: a manual rule is possible, behind a confirmation that names the risk. A consent banner can appear on any page until a visitor consents, and an unstyled banner is a compliance incident, not a layout bug.

Preview before you save

Click Scan pages first: the plugin fetches a sample of your pages (the Insights tracked pages plus the newest item of each post type, capped by "Pages to scan") and lists the files each one loads. Then click Preview: the tab shows, per sampled page, which files would stop loading, which would be kept and why, and roughly how many bytes leave the page. Nothing changes until you click Save Changes. Sampled pages are a sample, not your whole site.

Saving a rule purges the page cache, and every rule change is recorded on the Activity page, where it can be reverted. On the tab, "Switch every rule off on save" is the one-click revert for all rules; it keeps them so you can switch them back on.

Suggested rules

After a scan, the tab lists suggested rules for well-known plugins (WooCommerce and its blocks, Contact Form 7, WPForms, Gravity Forms, MailPoet, Elementor, Jetpack, Slider Revolution, Dashicons). A file is suggested only when its plugin's markup is absent from at least one sampled page, no other loaded file depends on it, and no rule already names it. The suggestion keeps the file on every sampled page that uses the plugin (and on every WooCommerce page when any of those pages is one) and removes it elsewhere; it shows how many sampled pages that covers and roughly how many bytes it saves.

Scan pages weekly (Advanced) repeats the scan once a week in the background so the suggestions and the preview stay current after content changes. The scan fetches your own pages from your own server and nothing is unloaded or saved by it. Off by default.

Clicking Add as a rule and preview does not save anything: the rule appears in the form below as a preview, showing exactly what it would remove, and it is applied only when you click Save Changes. Consent-banner files (Complianz) are never suggested. Sampled pages are a sample, not your whole site: if the plugin is used on a page the scan did not visit, add that page to Insights and scan again, or add the page to the rule's exceptions before saving.

Handles versus bundle files

With Combine on, the network panel shows one bundle, not the handles the rule names. The scan lists handles as WordPress sees them, before combining. A dequeued handle never reaches the bundle, so the bundle is rebuilt on its own.

Device-only rules

A rule can be limited to mobile or desktop. Those rules are inactive until Separate mobile cache is enabled on the Cache tab, because otherwise the page cache would serve one device's page to the other.

Hard-coded tags (advanced)

A file a theme prints directly, rather than enqueues, has no handle. "Also remove hard-coded tags" lets a rule with URL pattern as its target remove matching link and script tags from the page HTML. There is no dependency check for those, so it is slower and riskier; slider and carousel stylesheets are always refused, and a tag quoted inside a script is never touched. Use it only when a handle rule has no effect.

Page Audit finding

The Page Audit reports plugin files that load on a page nothing on it uses (for example a form plugin's stylesheet on a page with no form), with their size. It is advice, not a fix: add a rule on the Unused CSS/JS tab and preview it.

Debug header

With CACHINGOUT_DEBUG on, responses carry X-CachingOut-Assets: rules=3; unloaded=wc-blocks-style(r7f3a91); kept=dashicons(dep:theme-icons), naming each unloaded handle with the rule that did it and each kept handle with the reason.

WP-CLI

wp cachingout assets scan [--url=<url>] [--pages=<n>]
wp cachingout assets list [--url=<url>]
wp cachingout assets rules
wp cachingout assets preview
wp cachingout assets suggest
wp cachingout assets revert --id=<rule> | --all

Performance results vary depending on hosting, theme, plugins and traffic. Benchmark figures are measured on our published test suite and are not a guarantee of results on any specific site.

Troubleshooting

A form, slider or shop element lost its styling after I added an unload rule. The rule's scope covers a page that does use the plugin (a shortcode added to a second page is the classic case). Open the Activity page and revert the rule, or tighten its matchers; the preview shows which sampled pages would lose the file.

My unload rule has no effect. Either the file is a dependency of another loaded file (the preview and the debug header say dep:<handle>), or the tag is hard-coded by the theme rather than enqueued (use a URL-pattern rule with "Also remove hard-coded tags"), or the page is being served from the page cache (saving a rule purges it; check for a HIT in the debug header).

My site looks broken after enabling CSS optimization. Some themes and page builders inject critical styles in non-standard ways. The active theme's own stylesheet is already kept render-blocking; check the Page Audit for a Layout stylesheet deferred finding and use its Keep it render-blocking button, then add further exclusions one at a time until the issue resolves.

My menu turned into a vertical list of links (or the header lost its layout and a button) after switching on Optimize CSS Delivery, and it is fine again with it off. On versions before 1.21.4 the inline critical CSS could carry a theme's mobile-only rules without their media query: the parser kept only the first rule of each @media (...) block conditional and let the rest through as if they applied everywhere, so a phone-layout rule such as a vertical navigation or a hidden button was applied on desktop too, whether or not the theme stylesheet itself was kept render-blocking. Update to 1.21.4 or later, purge, and switch the feature back on; every inlined rule now keeps its media query. (1.21.3 separately fixed the minified theme stylesheet losing its protection with Minify CSS on, see "Minify CSS and the protections".) If the symptom appears on 1.21.4 or later, add a fragment of the affected stylesheet's file name to Excluded CSS as the immediate fix and report it. In every case measure both configurations with PageSpeed Insights and keep the higher one.

Optimize CSS Delivery is on, but my CSS is still render-blocking. PageSpeed (or Lighthouse) still flags "eliminate render-blocking resources" for one big stylesheet, or the plugin says it deferred my CSS and the report disagrees. Check the X-CachingOut-CSS debug header or the Page Audit's Layout stylesheets kept finding: the file named there with reason theme is one the plugin keeps render-blocking on purpose. If that file is a combined bundle (cache/cachingout/combined/<hash>.css), Combine CSS is on and the bundle contains a theme stylesheet, so the whole bundle inherited the theme's protection, plugin CSS included. On 1.21.4 and earlier that was every consecutive local stylesheet in one file; update to 1.21.5 or later, purge, and Combine CSS builds a protected bundle and a separate deferrable bundle, so only the theme's CSS stays render-blocking. On 1.21.5 or later a large protected file is the theme's own stylesheet: that is by design (deferring it causes a layout shift, see "Theme stylesheet and coverage guard"); the remaining levers are the theme reducing its stylesheet, the Unused CSS/JS tab for plugin files on pages that do not use them, and, only for a theme whose layout you have verified is covered, the cachingout_critical_css_theme_protected filter.

Optimize CSS Delivery made my PageSpeed score worse, not better. This happens on sites whose theme ships one large stylesheet: the protection keeps that sheet render-blocking, so the feature adds its inline critical block to every page and only defers small stylesheets, which saves less than the block costs. Switch it off and keep Minify CSS on; compare two warm runs of each configuration and keep the higher one. The feature earns its place on sites whose above-the-fold layout comes from several small stylesheets or a page builder, not from one theme bundle.

JavaScript features stopped working. Delay JS is the most likely culprit. Check your browser console for errors, then add the affected scripts to the Delay JS exclusion list.

jQuery is not defined or $ is not defined in the console after switching on Load jQuery in the footer. A script hard-coded into a header template uses jQuery before the footer. Switch the setting off, or attach that snippet to the jquery handle with wp_add_inline_script() so it moves with jQuery. See "Load jQuery in the footer".

jQuery is not defined after switching on Defer jQuery. An inline script on the page uses jQuery before the page has finished loading; the Filesystem Check row Defer jQuery (opt-in) names it by id. Wrap that snippet in a DOMContentLoaded listener, or switch the setting off and use Load jQuery in the footer. See "Defer jQuery, opt-in".

Load jQuery in the footer is on, but jQuery still loads in the head and PageSpeed still flags it. A script printed in the head depends on jQuery, so WordPress keeps jQuery there; the setting has no effect until that script moves. The Filesystem Check row names the script; with CACHINGOUT_DEBUG on, the X-CachingOut-jQuery header reads head:pinned-by=<handle>. Ask the theme or plugin that registers it to load it in the footer.

A carousel or slider only moves a few pixels on mobile, then a swipe opens the slide's link. The slider library (Swiper, Splide, Slick, Owl) is being delayed while the theme's inline initialiser runs at page load and fails with ReferenceError. Add the library name to the Delayed JS exclusions. See "Libraries that other code depends on" above.

A lightbox, animation, map or chart never initialises, with X is not defined in the console. Same cause and same fix: exclude that library from Delay JS.

Combining made my site slower. This is expected on HTTP/2+ servers. Disable combining and rely on HTTP/2 multiplexing instead.

A carousel, slider or gallery makes the page jump as it loads, and my CLS score is bad. The slider's stylesheet is being loaded asynchronously, so its slides stack vertically until it applies and then collapse to one row. The plugin keeps the common slider stylesheets render-blocking automatically; if yours is not covered, add a fragment of its file name to the Excluded CSS list. A min-height on the carousel will not help – the box is too tall beforehand, not too short. See "Stylesheets that lay out the page" above.

Lighthouse blames my hero (or another large block) for layout shift, but nothing about that element changes. Lighthouse names the element that MOVED the most, which is the victim of the shift, not its cause. Look at what sits above it in document order: a bar or banner injected or resized after the first paint (a JavaScript-built top bar with lazy images that have no width and height, a late notice, a web font that changes a heading's height) pushes everything below it down, and the largest element below is the one reported. Fix the element above: give its images width and height, reserve the bar's height in CSS, or render it on the server. On one site two "hero" shifts were both a JavaScript-rewritten top bar injecting two unsized images above it; fixing the bar took CLS from 0.15 to 0.001 with the hero untouched.

My Lighthouse score swings by 15-20 points between runs on the same page. Usually a layout shift that depends on whether a lazy-loaded image or a delayed script arrived inside the measurement window. Run the audit at least twice, and warm the cache first – a run taken immediately after a settings change or a purge measures an uncached page.

The whole top of the page moves once as it loads with Optimize CSS Delivery on, and CLS is close to 1. A deferred stylesheet lays out elements that are visible before the first paint and the inline critical CSS does not cover them. Since 0.1.21 the plugin keeps such a stylesheet render-blocking on its own (see "Theme stylesheet and coverage guard"). If it still happens, open the Page Audit: the Layout stylesheet deferred finding names the file and the selectors, and Keep it render-blocking fixes it in one click, recorded on the Activity page. A stylesheet on another host is not covered by the guard; add a fragment of its name to Excluded CSS.

My page is blank after enabling Optimize CSS Delivery. The critical CSS generator may have missed some styles. Disable this feature and use the exclusion list to help the generator identify all critical styles.

Free edition · WordPress.org

Get Caching Out for free

Good call. Page caching is the biggest single speed win, and it is on the moment you activate. Two ways to install the free edition:

Option 2

Upload the zip

Download the plugin zip, then go to Plugins → Add New Plugin → Upload Plugin and choose the file.

Download free plugin (.zip)

After you activate, caching is already running. Open Caching Out → Setup Wizard to see what is on and why.

Want every feature from day one? Pro from $59/yr