Skip to content

How it works#

Request flow#

1. The drop-in#

When WP_CACHE is true, WordPress includes wp-content/advanced-cache.php early in its bootstrap, before plugins and before the database is used. BB Cache's drop-in is seven lines: it defines BB_CACHE_ADVANCED_CACHE and includes wp-content/plugins/bb-cache/inc/serve.php.

No WordPress function is available at that point. Everything serve.php needs comes from one generated file.

2. config.php#

wp-content/cache/bb-cache/config.php is a PHP file that returns an array, written with var_export by BB_Cache_Settings::write_config(). It is rewritten on every settings save, on activation, by wp bb-cache config, by Tools → Reinstall advanced-cache.php and set WP_CACHE, and by the version self-heal described below.

Key Source
version The plugin version that wrote the file
page_cache, cache_mobile, cache_webp The settings of the same name
cache_lifespan The setting, converted from hours to seconds
cf_edge_ttl The setting; decides the Cache-Control header on a hit
cache_query_strings, reject_uri, reject_cookies, reject_ua The lists of the same name
cache_path The pages folder (informational: the drop-in resolves the folder from its own location)
hosts The host names the cache serves and stores for

The hosts list is the site's host name with and without www., filterable with bb_cache_hosts. It is derived from home_url() only in a trusted context: a settings save, WP-CLI, activation or the reinstall tool. An empty list turns the host check off. See Hosting notes for why.

Version self-heal. serve.php exports the config's version and hosts. On plugins_loaded, if the version is present and differs from the running plugin version, the plugin rewrites config.php with the current keys and keeps the existing host list. A plugin update therefore takes effect without a settings save. A missing config.php is not healed this way; see Troubleshooting.

3. Cache key and variants#

bb_cache_request_key() either returns a reason the request must not be cached, or the folder and file name for it. The checks run in this order:

Order Check Reason on bypass
1 Request method is not GET method
2 WP_CLI, DOING_CRON, DOING_AJAX, REST_REQUEST or XMLRPC_REQUEST is defined, or DONOTCACHEPAGE is true context
3 URI starts with /wp-admin, /wp-login.php or /wp-cron.php, or contains .php, .xml, .txt or .json directly before the query string or the end uri
4 URI matches an entry of reject_uri (substring or regex) reject_uri
5 A cookie name starts with an entry of reject_cookies cookie
6 User agent matches an entry of reject_ua (regex, case-insensitive) ua
7 Query contains bb_nocache, preview, s, p, page_id or customize_changeset_uuid query
8 Query contains a parameter that is neither a known marketing parameter nor in cache_query_strings query
9 Host is not in the hosts list (when the list is not empty) host
10 Path contains .. or a NUL byte path

A theme's own debug query flags need no entry here: check 8 bypasses the cache for any parameter that is not on the allow list.

Marketing parameters never create a variant and never cause a bypass: utm_source, utm_medium, utm_campaign, utm_term, utm_content, utm_id, utm_expid, fbclid, gclid, gclsrc, dclid, msclkid, mc_cid, mc_eid, ref, fb_action_ids, fb_action_types, fb_source, age-verified, _ga, _gl, li_fat_id, mkt_tok and the hsa_* set (hsa_acc, hsa_cam, hsa_grp, hsa_ad, hsa_src, hsa_tgt, hsa_kw, hsa_mt, hsa_net, hsa_ver). A URL carrying only these is served from the same file as the bare URL.

File location.

wp-content/cache/bb-cache/pages/<host>/<path>/index[-https][-mobile][-webp][-q<hash>].html
Part When
<host> The request's Host header, lower-cased, with : replaced by _ when a port is present
<path> The decoded URL path with duplicate slashes collapsed; the home page has no path folder
-https HTTPS is on, the server port is 443, or X-Forwarded-Proto is https
-mobile cache_mobile is on and the user agent is a phone (tablets count as desktop)
-webp cache_webp is on and the Accept header contains image/webp
-q<hash> The query contains allow-listed parameters; the hash is the MD5 of those parameters, sorted by name

Each file has a .gz sibling when PHP's gzencode is available.

4. Serving a hit#

If the file exists and is younger than the lifespan, serve.php sends it and exits. The response carries:

Header Value
X-BB-Cache HIT
Last-Modified, ETag From the file's modification time. A matching If-None-Match or If-Modified-Since gets 304 Not Modified
Cache-Control public, max-age=0, s-maxage=<cf_edge_ttl> when the edge TTL is set, otherwise no-cache, must-revalidate
Vary Accept-Encoding, Cookie, plus User-Agent when cache_mobile is on and Accept when cache_webp is on
Content-Encoding: gzip When the client accepts gzip, the .gz sibling is current and zlib.output_compression is off

A file older than the lifespan is deleted on the spot and the request continues as MISS (expired). With a lifespan of 0, files never expire by age.

5. Storing on the output buffer#

On a miss, WordPress renders the page. BB_Cache_Html starts one output buffer at template_redirect (priority -1000) for front-end HTML requests. It is not started for admin, feeds, robots.txt, trackbacks, AJAX, REST, XML-RPC, or when ?bb_raw is present. When the buffer closes, the HTML passes through the bb_cache_html_output filter; output that does not start with <!DOCTYPE html or <html is left alone. If an optimiser throws, the error is logged and the original HTML is sent.

The page cache is the last filter (priority 1000). It first sets the edge Cache-Control header (see Cloudflare), then stores the HTML if is_storable() agrees:

  • the drop-in produced a cache target and the reason was miss or expired
  • the visitor is not logged in
  • the request is not a 404, search, preview, feed, trackback, robots.txt, admin or Customizer preview request
  • DONOTCACHEPAGE is not true, the response code is 200 and the post is not waiting for a password
  • with WooCommerce active, the page is not cart, checkout or account
  • no Set-Cookie header is being sent, and any Content-Type header is text/html
  • the HTML contains </html> and is at least 255 bytes long
  • the bb_cache_storable filter does not return false

The file is written to a temporary name and renamed into place, so a reader never sees a partial file.

6. Purge triggers#

Trigger What is purged
save_post, deleted_post (not revisions, autosaves or auto-drafts; a draft is skipped unless transition_post_status has fired in the same request) The post's URLs (below)
transition_post_status to or from publish The post's URLs
A new approved comment; any comment status change The comment's post's URLs
edited_terms, delete_term Everything
switch_theme, customize_save_after, wp_update_nav_menu Everything
Option updates: sidebars_widgets, permalink_structure, page_on_front, show_on_front Everything
acf/save_post for an ACF options page Everything
upgrader_process_complete, activated_plugin, deactivated_plugin Everything
Saving BB Cache settings Everything, and the minified files
Local Google Fonts copy fetched Everything
Admin bar and Tools buttons, wp bb-cache purge As requested

A post's URLs are: its permalink (and everything beneath it, which covers paged and child URLs), the home page, the post type archive, the posts page for posts, the term archives of every taxonomy of its post type, the author archive and the parent's permalink. The list passes through the bb_cache_purge_post_urls filter. When purge_all_on_update is on, any post purge empties the whole cache instead.

Expiry. An hourly cron event (bb_cache_sweep) deletes files older than the lifespan. The drop-in also deletes an expired file when it meets one.

Hooks for your own code.

Hook Type Use
bb_cache_purge_all action to call do_action( 'bb_cache_purge_all' ) empties the page cache
bb_cache_purge_url action to call do_action( 'bb_cache_purge_url', $url ) removes the variants of one URL
bb_cache_purged action to listen to Fired after each purge with ( 'url', $url ), ( 'post', $post_id ) or ( 'all', null )

Since 0.3.0 the url event fires even when nothing was stored for that URL, because Varnish or Cloudflare may still hold it. A full purge also records the time in the bb_cache_last_purge option and, with preload_after_purge on, starts a preload.

What is and is not cached#

Cached: GET requests for public HTML pages that returned 200, from anonymous visitors without session cookies, with no query string or only marketing or allow-listed parameters, on the site's own host names.

Never cached:

  • logged-in users, commenters with the comment cookie, visitors who unlocked a password-protected post (by default cookie list)
  • POST and every other non-GET method, including HEAD
  • admin, login, cron, REST, AJAX, XML-RPC and feed requests; URLs ending in .php, .xml, .txt, .json
  • 404 pages, search results, previews, password-protected posts
  • responses that set a cookie, non-200 responses, non-HTML responses
  • any query string with a parameter that is not a marketing parameter and not in cache_query_strings
  • WooCommerce cart, checkout and account pages
  • anything matched by reject_uri, reject_cookies or reject_ua

The optimiser pipeline#

Optimisers run only when the request is being optimised: the visitor is logged out, or optimize_logged_in is on. The bb_cache_optimize filter can override the decision per request. Each optimiser registers its filter only when one of its settings is on. Comments, <noscript> and <template> blocks are masked while the tag scanners run, so their contents are never rewritten.

Priority Class Steps, in order
10 BB_Cache_Minify Combine Google Fonts links; minify and combine CSS (and inline the critical_inline sheets when critical CSS is on); minify and combine JS
20 BB_Cache_JS Per script: delay if eligible, otherwise add defer if eligible. Insert the delay loader after <meta charset>
30 BB_Cache_Media Mark the LCP image (when lcp_preload is on) so it stays eager; image dimensions; lazy-load images; YouTube preview; lazy-load iframes
35 BB_Cache_Boost Local Google Fonts; critical CSS and async stylesheets; srcset; WebP swap; lazy videos; LCP preload; preconnect; content-visibility; HTML minify
40 BB_Cache_Preload dns-prefetch, preconnect and font preload tags; hover prefetch script
1000 BB_Cache_Page_Cache Edge headers; store

The order matters in a few places:

  • CSS is combined before critical CSS runs, so the async swap applies to the combined sheets.
  • Scripts are minified and combined before delay and defer, so a combined file is delayed or deferred as one.
  • The LCP image is marked before lazy loading and preloaded after srcset and WebP have run, so the preload carries the final URLs.
  • HTML minify is last in Boost, after the masked blocks are restored.

File optimisation details#

  • Only local files are touched: URLs on the site's own host (or the content URL's host; filter bb_cache_local_hosts) that resolve, after realpath, to a readable file under wp-content or wp-includes.
  • CSS. A stylesheet link qualifies when its URL ends in .css, it has no onload or disabled attribute and it is not excluded. Consecutive qualifying sheets with media="all" and nothing but whitespace between them combine into one file; a sheet with another media value is minified on its own. A group of several sheets in which one contains @import is left untouched. Minification removes comments (not /*! … */) and whitespace, and rewrites relative url() paths to root-relative ones.
  • JS. A script qualifies when it has a src, a JavaScript type (or none), no async or nomodule attribute and is not excluded. With concat_js on, consecutive qualifying scripts with the same defer state combine. A lone .min.js file is left as it is. Minification removes comments and leading whitespace only; newlines are kept so code that depends on automatic semicolon insertion keeps its meaning.
  • Output. wp-content/cache/bb-cache/min/css/<hash>.css and min/js/<hash>.js. The hash covers the plugin version and every source file's path, size and modification time, so a rebuilt source file gets a new URL.
  • Google Fonts combine. Two or more fonts.googleapis.com stylesheet links using the same API version become one link with display=swap, plus a preconnect to fonts.gstatic.com.

JavaScript details#

  • Delay. A delayed script becomes <script type="bb/delay"> (with data-src for external scripts). An inline loader runs them in document order on the first keydown, mousedown, mousemove, touchstart, touchmove or wheel event, or after delay_js_timeout seconds. While they run, listeners they register for DOMContentLoaded, load and readystatechange are captured and replayed, document.write is redirected, and the window receives a bb-delayed-loaded event at the end.
  • In thirdparty mode a script is delayed only when its src is on another host, or when it is inline and refers to another host or matches a built-in list of tag-manager and tracker patterns. In all mode every script is delayed except the exclusions.
  • Never delayed: module scripts, non-JavaScript types (JSON-LD, templates, import maps), empty inline scripts, scripts with a data-no-delay attribute, and anything matching delay_js_exclusions (tested against the src or id, the opening tag, and the body of inline scripts).
  • Defer. Scripts with a src that have neither defer nor async, are not modules and do not match exclude_defer_js get the defer attribute.

Media details#

  • An image is left eager when it is among the first lazyload_skip images, sits in the first screen, already has a loading or fetchpriority attribute, has data-no-lazy, or matches exclude_lazyload. The first screen is the markup before the second <section tag, or the first 40 KB after <body> when the page has fewer than two.
  • Lazy images get loading="lazy" and, when absent, decoding="async".
  • A YouTube iframe (youtube.com/embed/… or youtube-nocookie.com/embed/…) becomes a div.bb-yt with the video's hqdefault.jpg thumbnail from i.ytimg.com and a play button. The player iframe is created on click.

Boost details#

  • Critical CSS: see Critical CSS.
  • Local Google Fonts. The first request that meets a Google Fonts link schedules a cron event. The event downloads the CSS and the font files it references into wp-content/cache/bb-cache/fonts, then purges the page cache. From then on the CSS is inlined as <style id="bb-fonts-<hash>">. Until the copy exists, the Google link stays.
  • WebP. For .jpg, .jpeg and .png URLs in src, data-src, srcset and data-srcset of <img> and <source>, a local sibling is used when one exists: name.webp, name.jpg.webp, or the same path under wp-content/smush-webp/ or uploads/smush-webp/. The plugin does not create WebP files.
  • LCP preload. In order: an element in the first screen (or any element whose class contains hero or masthead within the first 120 KB) with an inline background image is preloaded; failing a raster background, a <video poster> in the first 60 KB; failing both, the first non-lazy, non-SVG <img> that is not small (width under 200) and not a logo, icon, avatar or badge by class (unless its width is 600 or more) gets fetchpriority="high" and a preload carrying its srcset and sizes. An image that is already preloaded is not preloaded twice. A --bg-m custom property in the same inline style is treated as a phone-sized candidate and preloaded under (max-width: 800px), with the full size under (min-width: 801px); that breakpoint is a constant taken from the originating theme.
  • Responsive images. An <img> under /wp-content/uploads/ without srcset gets one from its attachment's registered sizes, with sizes="(max-width: Wpx) 100vw, Wpx". data-no-srcset opts an image out.
  • Lazy videos. A <video> loses its src (kept as data-src) and gets preload="none". Videos in the first screen load after the window load event; the others load when within 300px of the viewport. autoplay is restored at that point. data-no-lazy opts a video out.
  • Preconnect. Up to six third-party hosts found in non-delayed script src and stylesheet href attributes.
  • content-visibility. Elements (section, div, article, footer) whose class matches a listed class name, after the first content_visibility_skip matches, get the class bb-cv (content-visibility:auto; contain-intrinsic-size:auto 900px).
  • HTML minify. Removes leading and trailing whitespace per line, blank lines and comments (conditional comments are kept) outside pre, textarea, script and style.

Opt-out attributes#

Attribute On Effect
data-no-delay <script> Never delayed
data-no-lazy <img>, <iframe>, <video> Never lazy-loaded
data-no-srcset <img> No srcset added
data-no-async <link rel="stylesheet"> Stays render-blocking when critical CSS is active
data-no-prefetch <a> Not prefetched on hover

Filters#

Filter Arguments Use
bb_cache_html_output $html The pipeline itself; add your own step at a priority between the ones above
bb_cache_optimize $optimize, $html Decide whether the optimisers run for this request
bb_cache_storable true, $html Veto storing a response
bb_cache_purge_post_urls $urls, $post Change the URLs purged for a post
bb_cache_hosts $hosts Change the host names the cache serves and stores for
bb_cache_local_hosts $hosts Hosts whose asset URLs map to local files
bb_cache_critical_key $key Change the legacy per-template critical CSS key
bb_cache_critical_css $css, $html Change the composed critical CSS