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
missorexpired - the visitor is not logged in
- the request is not a 404, search, preview, feed, trackback,
robots.txt, admin or Customizer preview request DONOTCACHEPAGEis not true, the response code is200and the post is not waiting for a password- with WooCommerce active, the page is not cart, checkout or account
- no
Set-Cookieheader is being sent, and anyContent-Typeheader istext/html - the HTML contains
</html>and is at least 255 bytes long - the
bb_cache_storablefilter 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)
POSTand every other non-GETmethod, includingHEAD- 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-
200responses, 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_cookiesorreject_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
srcsetand 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, afterrealpath, to a readable file underwp-contentorwp-includes. - CSS. A stylesheet link qualifies when its URL ends in
.css, it has noonloadordisabledattribute and it is not excluded. Consecutive qualifying sheets withmedia="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@importis left untouched. Minification removes comments (not/*! … */) and whitespace, and rewrites relativeurl()paths to root-relative ones. - JS. A script qualifies when it has a
src, a JavaScripttype(or none), noasyncornomoduleattribute and is not excluded. Withconcat_json, consecutive qualifying scripts with the samedeferstate combine. A lone.min.jsfile 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>.cssandmin/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.comstylesheet links using the same API version become one link withdisplay=swap, plus a preconnect tofonts.gstatic.com.
JavaScript details#
- Delay. A delayed script becomes
<script type="bb/delay">(withdata-srcfor external scripts). An inline loader runs them in document order on the firstkeydown,mousedown,mousemove,touchstart,touchmoveorwheelevent, or afterdelay_js_timeoutseconds. While they run, listeners they register forDOMContentLoaded,loadandreadystatechangeare captured and replayed,document.writeis redirected, and the window receives abb-delayed-loadedevent at the end. - In
thirdpartymode a script is delayed only when itssrcis 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. Inallmode 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-delayattribute, and anything matchingdelay_js_exclusions(tested against thesrcorid, the opening tag, and the body of inline scripts). - Defer. Scripts with a
srcthat have neitherdefernorasync, are not modules and do not matchexclude_defer_jsget thedeferattribute.
Media details#
- An image is left eager when it is among the first
lazyload_skipimages, sits in the first screen, already has aloadingorfetchpriorityattribute, hasdata-no-lazy, or matchesexclude_lazyload. The first screen is the markup before the second<sectiontag, 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/…oryoutube-nocookie.com/embed/…) becomes adiv.bb-ytwith the video'shqdefault.jpgthumbnail fromi.ytimg.comand 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,.jpegand.pngURLs insrc,data-src,srcsetanddata-srcsetof<img>and<source>, a local sibling is used when one exists:name.webp,name.jpg.webp, or the same path underwp-content/smush-webp/oruploads/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
heroormastheadwithin 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 (widthunder 200) and not a logo, icon, avatar or badge by class (unless itswidthis 600 or more) getsfetchpriority="high"and a preload carrying itssrcsetandsizes. An image that is already preloaded is not preloaded twice. A--bg-mcustom 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/withoutsrcsetgets one from its attachment's registered sizes, withsizes="(max-width: Wpx) 100vw, Wpx".data-no-srcsetopts an image out. - Lazy videos. A
<video>loses itssrc(kept asdata-src) and getspreload="none". Videos in the first screen load after the windowloadevent; the others load when within 300px of the viewport.autoplayis restored at that point.data-no-lazyopts a video out. - Preconnect. Up to six third-party hosts found in non-delayed script
srcand stylesheethrefattributes. - content-visibility. Elements (
section,div,article,footer) whose class matches a listed class name, after the firstcontent_visibility_skipmatches, get the classbb-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,scriptandstyle.
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 |