WordPress Fragment Caching Revisited

Avatar of Ryan Burnette
Ryan Burnette on (Updated on )

The following is a guest post by Ryan Burnette. As you’ll read below, Ryan was working on a WordPress site that utilized a plugin that used the Instagram API to pull down photos. He was using it in a bit of a non-standard way that lead to lots of requests and a very slow site. In poking around at different solutions, he came across fragment caching. But unfortunately some of the information he found was outdated, so, like a good developer, he updated it. Here’s the backstory and journey.

We all know web performance is important. For developers who build custom WordPress themes, however, it’s pretty far down on the priority list when actually writing code. The code which renders elements on the page is usually written in the simplest, most friendly way possible using the functions that are available. This leads to code that is easily created, read, and maintained. It also leads to elements which have a very inefficient rendering process with extraneous loops and database queries.

A few extra milliseconds really start to add up. Compound this with increases in site traffic and major performance problems can arise.

Lots of really smart people have already applied their brains to this problem. The WordPress community has produced some great caching plugins. W3 Total Cache is one of them. I love them and use them frequently, but sometimes I don’t need all that power. I might want to avoid configuration or have elements which aren’t cache-friendly. It’s also nice to keep plugins to a minimum to avoid maintenance hassles down the road.

This led to me pursue a different approach. I wanted to use a very small amount of code to cache just a few elements on the page which are too clumsy to render on every load.

Fragment Caching

When a WordPress page loads, PHP is processed and the MySQL database is queried. Sometimes a block of code makes many queries and takes a while to run. Fragment caching takes the output of a code block and stores it so for a predetermined amount of time. When the code runs, as long as the time limit hasn’t elapsed, the block is ignored and the stored output is returned and printed onto the page.

Fragment caching is nothing new. WordPress core developer Marc Jaquith wrote about fragment caching. I later found a Gist that simplified Jaquith’s class into a function. I forked that and modified from there.

In WordPress versions before 2.5, WP_Cache objects could be used as Jaquith’s example demonstrates for persistent caching, or caching that lasts longer than one page load. The Transients API can create persistent database objects with a convenient expiration feature. My fragment caching snippet uses this method to store fragments.

Here is few lines of code can be included in the functions.php file, allowing any output to be cached as a fragment. Here’s the code.

function fragment_cache($key, $ttl, $function) {
  if ( is_user_logged_in() ) {
    call_user_func($function);
    return;
  }
  $key = apply_filters('fragment_cache_prefix','fragment_cache_').$key;
  $output = get_transient($key);
  if ( empty($output) ) {
    ob_start();
    call_user_func($function);
    $output = ob_get_clean();
    set_transient($key, $output, $ttl);
  }
  echo $output;
}

The function takes three arguments:

  • Key: a simple string which identifies the fragment. Notice that the function adds a prefix to avoid colliding with other transients. You can alter the prefix by editing the function or adding a filter that matches the 'fragmentcacheprefix' tag.

  • Time to live: a time in seconds for the cache to live. I usually make use of time constants. For example, DAYINSECONDS is 86400, the number of seconds in a day. This helps those of us who are too lazy for some simple math.

  • Function: the function which creates the output. This can be anything as the examples in this post show.

Usage Examples

Using fragment caching is as easy as wrapping some HTML and PHP in function.

Here’s some code that a developer might write on a WordPress site or application.

<p>Here's some HTML.</p>

<?php
// Here's some PHP
$args = array(
  'post_type' => 'my_data',
  'posts_per_page' => -1
);
$posts = get_posts($args);
foreach ( $posts as $p ) {
  echo '<pre>';
  echo get_post_meta($p,'some_meta',true);
  echo '</pre>';
}?>

<p>The PHP in this block runs and executes queries with every page load. :(</p>

Here’s the same code implemented using the fragment caching snippet. Notice we’re using HTML and PHP and that gets caught by the function and cached.

Let’s recap the function’s three arguments:

  • A tag to represent the cache. Here's a tip. If this code varies per page, concatenate the post ID into the tag to create a separate cache for each page. This would be important if the main loop is being fragment cached.
  • The timeout. I usually use WordPress time constants, but any amount of time in seconds can be used.
  • The output code itself. Notice that it's kept inside a function. This function is passed into the fragment cache function. That's right, you can pass a function as an argument in PHP.
<?php
// After
fragment_cache('my_footer', DAY_IN_SECONDS, function() { ?>

<p>Here's some HTML.</p>

<?php
// Here's some PHP
$args = array(
  'post_type' => 'my_data',
  'posts_per_page' => -1
);
$posts = get_posts($args);
foreach ( $posts as $p ) {
  echo '<pre>';
  echo get_post_meta($p,'some_meta',true);
  echo '</pre>';
}
?>

<p>And everything this block outputs will be fragment cached. :)</p>

<?php }); ?>

Examples

Here are a few examples of places where I spare my database the effort of rendering an element more often than it really needs to.

Custom Footers

The most common place where I implement this function is in a custom footer. I’ll often make a footer that contains not only WordPress menus, but menus I’m generating based on the get_posts() function and additional get_post_meta() functions for each iterated post. I’ve found many cases where it’s taking 100-200 milliseconds to render a big footer. Fragment caching makes the load time of such elements irrelevant.

Tables Of Data

WordPress has been gaining popularity as an application development platform. There’s a lot of buzz about this right now. Like it or not, people are going to build apps in WordPress. This often leads to situation where what would normally be a group of database objects are stored as posts in a custom post type. Each attribute becomes a piece of meta on that post rather than an attribute of a true database object. Querying and rendering a table of data stored in this way takes a long time. Fragment caching it can solve the problem.

Embarrassingly Long Loops

There are thousands of embarrassingly long and convoluted loops out there. I’ve written a few of them. No matter what inefficient piece of code you have written, you can stick it in a fragment cache and it will load fast.

A Test Case

I’m the webmaster for STUDIOCRIME, a site that aggregates street art videos. Update December 2019: link removed as the site is gone and now is spam.

WordPress provides a fantastic, simple CMS for our curators to use when posting and organizing video content for the site. The video collection pages load over 80 posts each time they viewed. Each of these iterations also queries the database for post meta.

We’re also displaying a lot of content in the sidebar using a plugin authenticates and pulls data from the Instagram API. The plugin wasn’t meant to be used in quite the way we’re using it. Each Widget instantiates the plugin separately. This leads to very long load times.

It sure was quick and easy to build, but milliseconds here and there added up to a page which takes between 1500 and 5000 milliseconds to render. Five seconds is a long time when waiting for a web page to load.

We chose not to use a caching plugin like W3 Total Cache because decisions about how a page should load and track user data within the PHP. Page caching would keep this PHP from running.

This presented the perfect opportunity to both use fragment caching and to test the gains that can be realized by caching fragments which are slow to load.

I ran these tests using Apache Bench. Apache Bench makes one or more requests either concurrently or back-to-back and reports the time it took the web server to serve the pages. Note that without caching a single request took about three times longer to load. Compound this with multiple requests and the time it takes to get a response gets pretty high, 3 to 5 seconds. Fragment caching the slow parts of the site got the times back down and gave us the performance we needed under heavier loads.

These tests show the rendering times for a single page under a concurrent load of 10, 100 and 1000 requests.

Apache Bench Test Without Caching With Caching
10 Requests 1426 ms 518 ms
100 Requests 3498 ms 658 ms
1000 Requests 5116 ms 895 ms

Happy caching!