A quick WordPress Super Cache fix

December 7th, 2024. Tagged: performance, WordPress


When you use a bog-standard WordPress install, the caching header in the HTML response is

Cache-Control: max-age=600

OK, cool, this means cache the HTML for 10 minutes.

Additionally these headers are sent:

Date: Sat, 07 Dec 2024 05:20:02 GMT
Expires: Sat, 07 Dec 2024 05:30:02 GMT

These don't help at all, because they instruct the browser to cache for 10 minutes too, which the browser already knows. These can actually be harmful in cases of clocks that are off. But let's move on.

WP Super Cache

This is a plugin I installed, made by WP folks themselves, so joy, joy, joy. It saves the generated HTML from the PHP code on the disk and then gives that cached content to the next visitor. Win!

However, I noticed it ads another header:

Cache-Control: max-age=3, must-revalidate

And actually now there are two cache-control headers being sent, the new and the old:

Cache-Control: max-age=3, must-revalidate
Cache-Control: max-age=600

What do you think happens? Well, the browser goes with the more restrictive one, so the wonderfully cached (on disk) HTML is now stale after 3 seconds. Not cool!

A settings fix

Looking around in the plugin settings I see there is no way to fix this. There's another curious setting though, disabled by default:

[ ] 304 Browser caching. Improves site performance by checking if the page has changed since the browser last requested it. (Recommended)
304 support is disabled by default because some hosts have had problems with the headers used in the past.

I turned this on. It means that instead of a new request after 3 seconds, the repeat visit will send an If-Modified-Since header, and since 3 seconds is a very short time, the server will very likely respond with 304 Not Modified response, which means the browser is free to use the copy from the browser cache.

Better, but still... it's an HTTP request.

A config fix

Then I had to poke around the code and saw this:

// Default headers.
$headers = array(
  'Vary'          => 'Accept-Encoding, Cookie',
  'Cache-Control' => 'max-age=3, must-revalidate',
);

// Allow users to override Cache-control header with WPSC_CACHE_CONTROL_HEADER
if ( defined( 'WPSC_CACHE_CONTROL_HEADER' ) && ! empty( WPSC_CACHE_CONTROL_HEADER ) ) {
  $headers['Cache-Control'] = WPSC_CACHE_CONTROL_HEADER;
}

Alrighty, so there is a way! All I needed to do was define the constant with the header I want.

The new constant lives in wp-content/wp-cache-config.php - a file that already exists, created by the cache plugin.

I opted for:

define(
  'WPSC_CACHE_CONTROL_HEADER',
  'max-age=600, stale-while-revalidate=100'
);

Why 600? I'd do it for longer but there's this other Cache-Control 600 coming from who-knows-where, so 600 is the max I can do. (TODO: figure out that other Cache-Control and ditch it)

Why stale-while-revalidate? Well, this lets the browser use the cached response after the 10 minutes while it's re-checking for a fresher copy.

Some WebPageTest tests

1. The repeat visit as-is, meaning the default less-than-ideal WP Super Cache behavior:
https://www.webpagetest.org/result/241207_AiDcHR_1QT/

Here you can see a new request for a repeat view, because 3 seconds have passed.

2. With the 304 setting turned on:
https://www.webpagetest.org/result/241207_AiDc4D_1QQ/

You can see a request being made that gets a 304 Not Modified response

3. Finally with the fix, the new header coming from the new constant:
https://www.webpagetest.org/result/241207_AiDcVT_1R8/

Here you can see no more requests for HTML, just one for stats. No static resources either (CSS, images, JS are cached "forever"). So the page is loaded completely from the browser cache.

Comments? Find me on BlueSky, Mastodon, LinkedIn, Threads, Twitter