How To: Use Transients To Speed Up WordPress Templates

Background on how caching works in WordPress, and when it makes sense to use transients, is in the introduction to this post, Using WordPress Transients To Speed Up Your PHP-Heavy Templates: Introduction.

WordPress transients are a simple way to store markup in the WordPress database for short periods of time.

In effect, transients are a kind of mini-cache. They allow us to store finished markup, generated by a template, that’s computationally expensive to create — such as a custom WP Query.

“Transients” are called that because they eventually expire. When some period of time is up (or we intentionally replace or expire the transient), WordPress can rebuild that transient and store it again for some period of time.

This has the net effect of speeding up page load times. Rather than the template having to repeatedly crunch the same code, over and over, to produce the same chunk of markup, the webserver does it once — and serves up that saved markup to all subsequent users.

That’s a lot easier on the server, so it makes pages load faster and reduces errors that can be created when a webserver is under stress.

Many people call transients “fragment caching.” That’s what they were called when they were first introduced, and it’s perfectly OK to call them that.
Patrons at Mathew's Pub in Portland, ME on April 4, 2014. © Ben McCanna via Flickr. All rights reserved, used by permission.
Patrons at Mathew’s Pub in Portland, ME on April 4, 2014. © Ben McCanna via Flickr. All rights reserved, used by permission.

Create, Get And Expire Transients

Working with transients is basically a two-step process:

  1. Check if the transient exists. It won’t exist if it hasn’t been created yet, or it has expired.
  2. If the transient exists, get it and show it. If it doesn’t, do whatever it takes to create the transient, show it, and then store it.

The way we check for whether a transient exists is the conveniently named function get_transient. It takes a single argument: the name of the transient we wish to retrieve.

get_transient('my_transient');

If the transient has expired, or was never created, this will return Boolean false; if the transient exists, it’s returned.

The way we create a transient is with the also conveniently named set_transient. It takes three arguments: the name we assign to the transient, the code to be stored, and the time, in seconds from now, when that transient will expire.

set_transient('my_transient', "<p>Hello world!</p>", 3600); //expires in 1 hour

This function returns Boolean true if the transient was set and Boolean false if it wasn’t.

Finally, we can intentionally destroy a transient with delete_transient, which takes a single argument: the name of the transient to be destroyed.

delete_transient('my_transient');

As with set_transient, this returns Boolean true on success, Boolean false on failure.

Example 1: Store The Markup Produced By A Complicated WP Query

Let’s get down to the nitty gritty of why we might want to use a transient, rather than caching the entire page.

A good example for this use is a complicated WP Query that can’t easily be cached by a file-based cache plugin, such as WP Super Cache.

A memory-based caching plugin, such as Batcache or W3 Total Cache, can store complex WP Queries in memory.

However, those plugins do so on an every-query basis; that is, all objects and queries are stored for the same duration.

So even if you’re using a memory-based cache, you may still want to use a transient, as it will allow you to set a duration for holding onto this markup that’s different than the general memory cache expiration setting.

Let’s say, for example, you run a blog about nightlife in your city.

You have a custom post type slugged restaurant_review. In that post type, you have a metadata field named star_rating (containing an integer from 1-5), and another metadata field that’s named has_lounge (containing a 1 or 0).

Now, you want to display on a page a directory of “date night” restaurants. You’ll define that as all restaurants that have a lounge and a star rating of 4 or 5.

Here’s how that WP Query might look:

$output = "";
$query = new WP_Query(array(
	'post_type' => 'restaurant_review',
	'post_status' => 'publish',
	'meta_query' => array(
		array(
			'key' => 'star_rating',
			'value' => array(4, 5),
			'compare' => 'IN'
		),
		array(
			'key' => 'has_lounge',
			'value' => 1
		)
	),
	'orderby' => 'title',
	'order' => 'DESC'
));

if($query->have_posts()) {
	$output = "<ul>";
	while($query->have_posts()) {
		$query->the_post()
		$output .= '<li><a href="' . the_permalink() . '">' . the_title . '</a> (' . get_post_meta(get_the_ID(), 'star_rating', true) . ' stars)</li>';
	}
	$output .= "</ul>";
}
else {
	$output = "<ul><li>No date night restaurants found.</li></ul>";
}
echo $output;

This is a fairly complex query for WordPress to handle; if your database has hundreds of restaurant_review posts, it’s also pretty expensive.

Since we’re not constantly adding and deleting these reviews, we can certainly save this markup into a transient.

In this example, I’m going to set the transient that holds this markup to expire every 4 hours, but you could even set this to be a day or more, especially if you’re never going to add or remove another restaurant review.

To set the transient, again, we just wrap it in a get_transient variable; if that returns false, then we’ll run the query and store its output to the transient.

if(false === ($output = get_transient('date-night-restaurants'))) {
	$output = "";
	$query = new WP_Query(array(
		'post_type' => 'restaurant_review',
		'post_status' => 'publish',
		'meta_query' => array(
			array(
				'key' => 'star_rating',
				'value' => array(4, 5),
				'compare' => 'IN'
			),
			array(
				'key' => 'has_value',
				'value' => 1
			)
		),
		'orderby' => 'title',
		'order' => 'DESC'
	));

	if($query->have_posts()) {
		$output = "<ul>";
		while($query->have_posts()) {
			$query->the_post()
			$output .= '<li><a href="' . the_permalink() . '">' . the_title . '</a> (' . get_post_meta(get_the_ID(), 'star_rating', true) . ' stars)</li>';
		}
		$output .= "</ul>";
	}
	else {
		$output = "<ul><li>No date night restaurants found.</li></ul>";
	}
	set_transient('date-night-restaurants', $output, 4 * HOUR_IN_SECONDS);
}

echo $output;

Let’s look at this more closely:

Line 1 is shorthand for a number of steps. Reading right to left:

  • I set the value of $output to be either the transient named date-night-restaurants, if it exists, or Boolean false, if it does not.
  • I evaluate if $output is Boolean false using the PHP equivalence operator. I have to do this because sometimes, a transient will contain a 0, or some other value that could be interpreted by PHP as Boolean false. (This is called type safety and I’ve previously written about the pitfalls of PHP’s weak data types.)
  • If the value of $output is not Boolean false, then I know it contains my markup, because that’s the only other possible result of the get_transient function.

So as you can see, I am consolidating three steps in a single line of code: determine if the transient exists; if so, put it into the $output variable; otherwise, rebuild the transient.

Lines 2-31 are the same as the code I used earlier, when I wasn’t trying to make a transient.

Line 32 takes my $output variable and assigns it as a transient named date-night-restaurants.

The contents of the transient is the value of $output; and it expires 4 hours from now. The constant HOUR_IN_SECONDS is provided as part of the WordPress Transients API.

Now, every time I call this block of code on a template within the next 4 hours, WordPress will simply get the markup from its database and echo it out, rather than running the underlying WP Query.

Once the transient expires, in 4 hours, the next visitor to the template will cause WordPress to run the WP Query, store the result and echo out the markup.

Pubcrawlers and Johnny Cremains at Geno's Rock Club, Dec. 6, 2013
The Pubcrawlers and Johnny Cremains perform at Geno’s Rock Club, Portland, ME, on Dec. 6, 2013. © Ben McCanna via Flickr. All rights reserved, used by permission.

Example 2: Selectively Cache Parts Of A Page

When making a complicated landing page for a section of a website, a designer will often need to use multiple queries to show content based on different criteria.

Let’s stick with our example of a local nightlife blog. Let’s say you want to make a landing page on your site dedicated to live music.

Let’s also suppose you track the music scene three ways:

  • You use the aside post type, in taxonomy term music-gigs, to list off details of upcoming performances: the name of the act, where they are playing, when and what the cover charge would be.
  • You have a custom post type called music_reviews, which (conveniently enough) reviews recent performances.
  • You have another custom post type, music_venues, which gives details about places where live music is often performed.

By definition, the asides will update often, as gigs are added, cancelled and changed; the reviews would change whenever one was added or removed; and the venue profiles would seldom change.

If we’re using a standard cache, we would need to regenerate this page whenever we expect the gig list to change. But that’s probably quite often; more often than is necessary for the music reviews, and way more often than is needed for the venue profiles.

By using transients, we can “selectively cache” these parts. That is, we can generate transients for the gig list, music reviews and venue profiles, each with its own expiration period.

That way, the site isn’t working so hard to keep regenerating content that isn’t outdated, and our overall page performance is better for our total visits.

The template code to create our three distinct WordPress sections might look like this, before we add support for transients:

//get all asides that are in the category music-gigs
$gigs = new WP_Query(array(
	'post_status' => 'publish',
	'posts_per_page' => 10,
	'orderby' => 'date',
	'order' => 'DESC',
	'tax_query' => array(
		array(
			'taxonomy' => 'post_format',
			'field' => 'slug',
			'terms' => array(
				'post-format-aside'
			),
			'operator' => 'IN'
		),
		array(
			'taxonomy' => 'category',
			'field' => 'slug',
			'terms' => array(
				'music-gigs'
			),
			'operator' => 'IN'
		)
	)
));

echo '<div id="music-gigs"><h2>Upcoming gigs</h2><ul>';
if($gigs->have_posts()) {
	while($gigs->have_posts()) {
		$gigs->the_post();
		echo '<li><a href="' . the_permalink() . '"><h3>' . the_title() . '</h3></a>';
		echo the_excerpt() . '</li>';
	}
}
else {
	echo '<li>No upcoming gigs available.</li>';
}
echo "</ul></div>";

//get the music review posts
$reviews = new WP_Query(array(
	'post_type' => 'music_reviews',
	'post_status' => 'publish',
	'posts_per_page' => -1,
	'orderby' => 'title',
	'order' => 'ASC'
));

echo '<div id="music-reviews"><h2>Music reviews</h2>';
if($reviews->have_posts()) {
	while($reviews->have_posts()) {
		echo '<div class="music-review">';
		$reviews->the_post();
		echo '<a href="' . the_permalink() . '"><h2>' . the_title() . '</h2></a>';
		echo the_excerpt();
		echo "</div>";
	}
}
else {
	echo "<p>No venues available.</p>";
}
echo '</div>';

//get the music review posts
$reviews = new WP_Query(array(
	'post_type' => 'music_venues',
	'post_status' => 'publish',
	'posts_per_page' => -1,
	'orderby' => 'title',
	'order' => 'ASC'
));

echo '<div id="music-venues"><h2>Where to listen</h2>';
if($reviews->have_posts()) {
	while($reviews->have_posts()) {
		echo '<div class="music-venues">';
		$reviews->the_post();
		echo '<a href="' . the_permalink() . '"><h2>' . the_title() . '</h2></a>';
		echo the_excerpt();
		echo "</div>";
	}
}
else {
	echo "<p>No venues available.</p>";
}
echo '</div>';

Once again, to use transients, we simply ask if the transient is on hand for each of these sections. If so, we use the transient; if not, we recreate the transient.

In this case, we do it three times, because we have content expiring at three different times: 5 minutes for the gigs list, four hours for the music reviews, and one day for the venues listing.

//get all asides that are in the category music-gigs
if(false === ($output = get_transient('my-gig-asides-list'))) {
	//rebuild transient
	$gigs = new WP_Query(array(
		'post_status' => 'publish',
		'posts_per_page' => 10,
		'orderby' => 'date',
		'order' => 'DESC',
		'tax_query' => array(
			array(
				'taxonomy' => 'post_format',
				'field' => 'slug',
				'terms' => array(
					'post-format-aside'
				),
				'operator' => 'IN'
			),
			array(
				'taxonomy' => 'category',
				'field' => 'slug',
				'terms' => array(
					'music-gigs'
				),
				'operator' => 'IN'
			)
		)
	));
	
	//create transient's value
	$output = '<div id="music-gigs"><h2>Upcoming gigs</h2><ul>';
	if($gigs->have_posts()) {
		while($gigs->have_posts()) {
			$gigs->the_post();
			$output .= '<li><a href="' . the_permalink() . '"><h3>' . the_title() . '</h3></a>';
			$output .= the_excerpt() . '</li>';
		}
	}
	else {
		$output .= '<li>No upcoming gigs available.</li>';
	}
	$output .= "</ul></div>";
	
	//store transient in WP database; expires in 5 minutes
	set_transient('my-gig-asides-list', $output, 5 * MINUTE_IN_SECONDS);
}
//one way or another, we have a valid value in $output; echo it
echo $output;

//get the music review posts
if(false === ($output = get_transient('my-music-reviews'))) {
	$reviews = new WP_Query(array(
		'post_type' => 'music_reviews',
		'post_status' => 'publish',
		'posts_per_page' => -1,
		'orderby' => 'title',
		'order' => 'ASC'
	));

	$output = '<div id="music-reviews"><h2>Music reviews</h2>';
	if($reviews->have_posts()) {
		while($reviews->have_posts()) {
			$output .= '<div class="music-review">';
			$reviews->the_post();
			$output .= '<a href="' . the_permalink() . '"><h2>' . the_title() . '</h2></a>';
			$output .= the_excerpt();
			$output .= "</div>";
		}
	}
	else {
		$output .= "<p>No venues available.</p>";
	}
	$output .= '</div>';
	
	//this transient lasts 4 hours
	set_transient('my-music-reviews', $output, 4 * HOUR_IN_SECONDS);
}
echo $output;

//get the music review posts
if(false === ($output = get_transient('my-venue-reviews'))) {
	$reviews = new WP_Query(array(
		'post_type' => 'music_venues',
		'post_status' => 'publish',
		'posts_per_page' => -1,
		'orderby' => 'title',
		'order' => 'ASC'
	));

	$output = '<div id="music-venues"><h2>Where to listen</h2>';
	if($reviews->have_posts()) {
		while($reviews->have_posts()) {
			$output .= '<div class="music-venues">';
			$reviews->the_post();
			$output .= '<a href="' . the_permalink() . '"><h2>' . the_title() . '</h2></a>';
			$output .= the_excerpt();
			$output .= "</div>";
		}
	}
	else {
		$output .= "<p>No venues available.</p>";
	}
	$output .= '</div>';
	
	//this transient lasts one day
	set_transient('my-venue-reviews', $output, DAY_IN_SECONDS);
}
echo $output;

If you’re using a sitewide caching plugin, such as WP Super Cache or W3 Total Cache, you’d want to exempt this page from being cached.

That’s because, again, we’re effectively caching this page as it is, and on a selective basis, at that. If we ran it against our site’s general cache (or, for that matter, against a content delivery network), we wouldn’t get the expected result.

Rather than pulling the different section content at different times, we’d get whatever our caching plugin last stored. And that defeats the purpose of selective caching.

php superglobals
PHP superglobals include POST and GET vars, cookies, sessions and more. Copyright © 2003 by David Lechnyr. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/).

Example 3: Leveraging Superglobals

Finally, let’s see how transients help us cache data, without hurting our ability to read GET and POST variables, or retrieve a cookie or session variable, or otherwise access superglobal values.

For example, let’s suppose our nightlife blog has special content for “Insiders,” or people who have registered with our blog.

Those people can see special blurbs talking about advance ticket sales, special promotions or other information we don’t want unregistered viewers to see. So wisely, we leverage the WordPress user database, and anyone who’s registered as a subscriber can see these members-only blurbs.

Once again, we’ll assume that these members-only blurbs are in the form of aside posts, in the category “insiders.”

On a WordPress template, we’d catch whether someone is an Insider (that is, he’s logged into the website) by using the function current_user_can, and choosing ‘read’:

if(current_user_can('read')) {
	$blurbs = new WP_Query(array(
		'post_status' => 'publish',
		'posts_per_page' => 10,
		'orderby' => 'date',
		'order' => 'DESC',
		'tax_query' => array(
			array(
				'taxonomy' => 'post_format',
				'field' => 'slug',
				'terms' => array(
					'post-format-aside'
				),
				'operator' => 'IN'
			),
			array(
				'taxonomy' => 'category',
				'field' => 'slug',
				'terms' => array(
					'insiders'
				),
				'operator' => 'IN'
			)
		)
	));
	
	echo '<ul>';
	if($blurbs->have_posts()) {
		while($blurbs->have_posts()) {
			$blurbs->the_post();
			echo '<li><a href="' . the_permalink() . '"><h3>' . the_title() . '</h3></a>';
			echo the_excerpt() . '</li>';
		}
	}
	else {
		echo '<li>No insider information today.</li>';
	}
	echo '</ul>';
}

This, of course, is expensive, and we really should cache it to keep site performance strong.

Unfortunately, however, we can’t reliably access current_user_can from a cached page, because it requires reading session and cookie variables. In theory, we can read a specific cached page based on whether a user is logged in, but this, too, is unreliable.

It all has to do with the order in which a Web server goes about its business.

Accessing superglobals happens very early in the process of building a page, but it doesn’t necessarily happen before the Web server begins composing its response to the user; and even when it does, it doesn’t necessarily happen in time to supersede the sending of a cached page, or something else that the Web server considers set in stone.

Using transients allows us to “late bind” the markup we want to show, so we can be certain the Web server has had a chance to get the superglobal value we want, before it sends a response.

This is kind of like using jQuery’s $(document).ready() method to change the DOM after the page has fully loaded; by using PHP to load transients, based on evaluating a superglobal, we can both run our page outside of the general caching mechanism of our website, and get results similar to as though the entire page was cached.

Once again, it’s just a little reworking of our code, to check if the transient exists and rebuild it if it doesn’t:

if( current_user_can( 'read' ) ) {
	if( false === ( $output = get_transient( 'my-insider-news' ) ) ) {
		$blurbs = new WP_Query( 
			array(
				'post_status' => 'publish',
				'posts_per_page' => 5,
				'orderby' => 'date',
				'order' => 'DESC',
				'tax_query' => array(
					array(
						'taxonomy' => 'post_format',
						'field' => 'slug',
						'terms' => array( 'post-format-aside' ),
						'operator' => 'IN'
					),
					array(
						'taxonomy' => 'category',
						'field' => 'slug',
						'terms' => array( 'insiders' ),
						'operator' => 'IN'
					)
				)
			) 
		);

		$output = '<ul>';
		if($blurbs->have_posts()) {
			while($blurbs->have_posts()) {
				$blurbs->the_post();
				$output .= '<li><a href="' . the_permalink() . '"><h3>' . the_title() . '</h3></a>';
				$output .= the_excerpt() . '</li>';
			}
		} else {
			$output .= '<li>No insider information today.</li>';
		}
		$output .= '</ul>';

		//transient will last a half-hour
		set_transient('my-insider-news', $output, 30 * MINUTE_IN_SECONDS);
	}
	echo $output;
}

Code, Links And Credits

This code is on github: https://github.com/dougvdotcom/wordpress-template-transients.

All links in this post on delicious: https://delicious.com/dougvdotcom/use-transients-speed-wordpress-templates

Featured photo: Patrons at Mathew’s Pub in Portland, ME on April 4, 2014. © Ben McCanna via Flickr. All rights reserved, used by permission.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  • Check out the Commenting Guidelines before commenting, please!
  • Want to share code? Please put it into a GitHub Gist, CodePen or pastebin and link to that in your comment.
  • Just have a line or two of markup? Wrap them in an appropriate SyntaxHighlighter Evolved shortcode for your programming language, please!