Use Transients to speed up WordPress widgets

Use Transients to speed up WordPress Widgets

Transients API is one of the lesser-known WordPress API’s and not as widely used as it should be by theme and plugin authors. This Tutorial will shows you how you can speed up your WordPress site by “caching” your widgets using Transients.

What is the Transients API?

The Transients API in general is a very simple API to temporarily store cached information. Think of it as a key-value store with expiration stamp.

When should I use it?

Basically you should use it anywhere you:

  • Pull or fetch data from 3rd party site or API, like your latest tweets from Twitter API.
  • Run Complex and resource heavy functions and methods to generate HTML output.
  • Run Complex and resource heavy Database Queries, like recent posts or comments.
  • See fit to use it 🙂

How to use it?

The Transients API is very similar to the options API so if you’re already familiar with it, this will be a breeze.
There are 3 methods you need to know of to get started with the transient API:

set_transient

set_transient( $transient, $value, $expiration );

The set_transient function is used to set/update the value of a transient.
Parameters:

  • $transient – Transient name.(string) 45 or less characters unique identifier.
  • $value – Transient value. (mixed) if needed the value will be serialized before it is set.
  • $expiration – Transient name. Time until expiration in seconds from now, or 0 for never expires.

The set_transient function returns true if value was set else it returns false.

get_transient

get_transient( $transient); 

The get_transient function is used to get the value of a transient.
Parameters:

  • $transient – Transient name.(string) 45 or less characters unique identifier.

The get_transient function returns the value of the transient if it exists and has not expired, if the value does not exists or has expired then false is returned.

delete_transient

delete_transient( $transient); 

The delete_transient function is used to delete a transient.
Parameters:

  • $transient – Transient name.(string) 45 or less characters unique identifier.

The delete_transient function returns true if the transient was deleted and false if the deletion failed.

The Plan

Since most widgets are generated every single page load of the site they slow the page load time and cost server and bandwidth resources (in remote requests).

While some widgets do use transients the vast majority of then don’t and to cut down the page load time and save some resources we are going to create a simple plugin which will store the output of the widget in a transient and will use it on the next page load instead of rendering the output all over again.

Coding the plugin

The whole plugin is available for download at GitHub.
To start coding our plugin we create a new file named gwp_widget_cache.php and we load it up with Standard WordPress Plugin Information header

<?php
/*
Plugin Name: GWP Widget Cache
Plugin URI: 
Description: A plugin to cache WordPress Widgets using the Transients API, based on this tutorial https://generatewp.com/?p=10132
Version: 1.0
Author: Ohad Raz
Author URI: https://generatewp.com
*/

Then we Create a main class for the plugin with the methods we want to implement:

/**
* GWP_Widget_cache
*/
class GWP_Widget_cache{
	/**
	 * $cache_time 
	 * transient expiration time
	 * @var int
	 */
	public $cache_time = 43200; // 12 hours in seconds
	
	/**
	 * __construct 
	 * 
	 * Class constructor where we will call our filter and action hooks.
	 */
	function __construct(){}
	
	/**
	 * get_widget_key 
	 * 
	 * Simple function to generate a unique id for the widget transient
	 * based on the widget's instance and arguments
	 * 
	 * @param  array $i widget instance
	 * @param  array $a widget arguments
	 * @return string md5 hash
	 */
	function get_widget_key($i,$a){}

	/**
	 * _cache_widget_output 
	 * @param array     $instance The current widget instance's settings.
	 * @param WP_Widget $this     The current widget instance.
	 * @param array     $args     An array of default widget arguments.
	 * @return mixed array|boolean
	 */
	function _cache_widget_output($instance, $widget, $args){}

}//end GWP_Widget_cache class

Next we are going to implement the class constructor and we are going to use the handy `widget_display_callback` filter hook which returning false to it will effectively short-circuit display of the widget. So our constructor will look like this:

	 /**
	 * __construct 
	 * 
	 * Class constructor where we will call our filter and action hooks.
	 */
	function __construct($args= array()){
		add_filter( 'widget_display_callback', array($this,'_cache_widget_output'), 10, 3 );
	}

Moving on, we are going to implement the _cache_widget_output method which we just hooked to the `widget_display_callback`. This method will check if we already have a cached (in transient) version of the current widget. If we have a cached version then the method will out it and will skip the widget rendering, If we don’t have a cached version of the widget then the method will render the widget, store it in a transient and then output that widget. so we get something like this:

	/**
	 * _cache_widget_output 
	 * @param array     $instance The current widget instance's settings.
	 * @param WP_Widget $this     The current widget instance.
	 * @param array     $args     An array of default widget arguments.
	 * @return mixed array|boolean
	 */
	function _cache_widget_output($instance, $widget, $args){
		if ( false === $instance )
			return $instance;

		//simple timer to clock the widget rendering
		$timer_start = microtime(true);
		
		//create a unique transient ID for this widget instance
		$transient_name = $this->get_widget_key($instance,$args);

		//get the "cached version of the widget"
		if ( false === ( $cached_widget = get_transient( $transient_name ) ) ){
			// It wasn't there, so render the widget and save it as a transient
			// start a buffer to capture the widget output
			ob_start();
			//this renders the widget
			$widget->widget( $args, $instance );
			//get rendered widget from buffer
			$cached_widget = ob_get_clean();
			//save/cache the widget output as a transient 
			set_transient( $transient_name, $cached_widget, $this->cache_time);
		}

		//output the widget
		echo $cached_widget;
		//output rendering time as an html comment
		echo '<!-- From widget cache in '.number_format( microtime(true) - $timer_start, 5 ).' seconds -->';

		//after the widget was rendered and printed we return false to short-circuit the normal display of the widget 
		return false;	
	}

As you can see, first we check if $instance is not set to false, then on line 16 we generate a unique identifier for the current widget using get_widget_key method which we’ll implement in a sec. On line 19 we get the cached transient and we check if it exists. If it does we skip to line 32 and if not we render the widget on lines 20-26 and store it as a transient on line 28 so the next time (next page load) we wont have to render the widget again. Then on lines 32-34 we print out the widget we have in our $cached_widget variable and last we return false to skip the normal widget display since we already printed the widget. On more thing Worth to mention is the use of $this->cache_time which is a property within our GWP_Widget_cache class and by default I set it to 12 hours in seconds (60 * 60 * 12).

Side note:

In WordPress 3.5 a few constants were introduced to help us write time in seconds:

  • MINUTE_IN_SECONDS = 60 seconds
  • HOUR_IN_SECONDS = 3600 seconds
  • DAY_IN_SECONDS = 86400 seconds
  • WEEK_IN_SECONDS = 604800 seconds
  • YEAR_IN_SECONDS = 3153600 seconds

We can use them directly or mixed with an integer eg: HOUR_IN_SECONDS * 12 would be just the same as 60 * 60 * 12.

Getting back to our class, we need to implement the get_widget_key method, All we need to do here is simply serialize the instance and the arguments of the widget and hash the result using MD5.

	 /**
	 * get_widget_key 
	 * 
	 * Simple function to generate a unique id for the widget transient
	 * based on the widget's instance and arguments
	 * 
	 * @param  array $i widget instance
	 * @param  array $a widget arguments
	 * @return string md5 hash
	 */
	function get_widget_key($i,$a){
		return 'WC-' . md5( serialize( array( $i, $a ) ) );
	}

So now our plugin file should look like this:

<?php
/*
Plugin Name: GWP Widget Cache (Cached!)
Plugin URI: 
Description: A plugin to cache WordPress Widgets using the Transients API, based on this tutorial https://generatewp.com/?p=10132
Version: 1.0
Author: Ohad Raz
Author URI: https://generatewp.com
/**
* GWP_Widget_cache
*/
class GWP_Widget_cache{
	/**
	 * $cache_time 
	 * transient exiration time
	 * @var int
	 */
	public $cache_time = 43200; // 12 hours in seconds
	
	/**
	 * __construct 
	 * 
	 * Class constructor where we will call our filter and action hooks.
	 */
	function __construct($args= array()){
		add_filter( 'widget_display_callback', array($this,'_cache_widget_output'), 10, 3 );
	}
	
	/**
	 * get_widget_key 
	 * 
	 * Simple function to generate a unique id for the widget transient
	 * based on the widget's instance and arguments
	 * 
	 * @param  array $i widget instance
	 * @param  array $a widget arguments
	 * @return string md5 hash
	 */
	function get_widget_key($i,$a){
		return 'WC-' . md5( serialize( array( $i, $a ) ) );
	}

	/**
	 * _cache_widget_output 
	 * @param array     $instance The current widget instance's settings.
	 * @param WP_Widget $this     The current widget instance.
	 * @param array     $args     An array of default widget arguments.
	 * @return mixed array|boolean
	 */
	function _cache_widget_output($instance, $widget, $args){
		if ( false === $instance )
			return $instance;

		//simple timer to clock the widget rendring
		$timer_start = microtime(true);
		
		//create a uniqe transient ID for this widget instance
		$transient_name = $this->get_widget_key($instance,$args);

		//get the "cached version of the widget"
		if ( false === ( $cached_widget = get_transient( $transient_name ) ) ){
			// It wasn't there, so render the widget and save it as a transient
			// start a buffer to capture the widget output
			ob_start();
			//this renders the widget
			$widget->widget( $args, $instance );
			//get rendered widget from buffer
			$cached_widget = ob_get_clean();
			//save/cache the widget output as a transient 
			set_transient( $transient_name, $cached_widget, $this->cache_time);
		}

		//output the widget
		echo $cached_widget;
		//output rendering time as an html comment
		echo '<!-- From widget cache in '.number_format( microtime(true) - $timer_start, 5 ).' seconds -->';

		//after the widget was rendered and printed we return false to short-circuit the normal display of the widget 
		return false;	
	}

}//end GWP_Widget_cache class

//instantiate the class
add_action( 'plugins_loaded', 'GWP_Widget_cache_init' );
function GWP_Widget_cache_init() {
    $GLOBALS['GWP_Widget_cache'] = new GWP_Widget_cache();
}

All i added is a simple function to instantiate the class hooked to the plugins_loaded action hook (lines 84-88) and i got a working plugin.

The results

After activating the plugin i ran a simple test to see how long the widgets take to render,The first page load which is when none of the widgets are cached yet I got:
before
as you can see its a sidebar with 6 widgets that take about 0.22 seconds (in average) to render each and over all about 1.66852 seconds to render.
On the second page load I got:
after
Right away you can see how much we have improved, each widget takes about 0.0006 seconds (in average) when loaded from transients and over all time of 0.00384 seconds to render all 6 widgets that is about 434% faster.

Making it better

As you can see using this we get amazing results but there is just one thing wrong, Not every widget needs or should be cached for example the native Meta widget which displays Log-In link to guests and Site Admin and Log out links to logged in users. So get around that problem we are going to add a simple check-box to allow the user to define if the widget needs to be cached or not.

To add our check-box we are going to use in_widget_form action hook and we are going to hook a function to it that well show the check box inside the widget panel:

	 /**
	 * in_widget_form
	 * this method displays a checkbox in the widget panel
	 * 
	 * @param WP_Widget $t     The widget instance, passed by reference.
	 * @param null      $return   Return null if new fields are added.
	 * @param array     $instance An array of the widget's settings.
	 * 
	 */
	function in_widget_form($t,$return,$instance){
		$instance = wp_parse_args( 
			(array) $instance, 
			array( 
				'title' => '', 
				'text' => '', 
				'wc_cache' => null
			)
		);

		if ( !isset($instance['wc_cache']) )
        	$instance['wc_cache'] = null;
        ?>
        <p>
        	<input id="<?php echo $t->get_field_id('wc_cache'); ?>" name="<?php echo $t->get_field_name('wc_cache'); ?>" type="checkbox" <?php checked(isset($instance['wc_cache']) ? $instance['wc_cache'] : 0); ?> />
        	<label for="<?php echo $t->get_field_id('wc_cache'); ?>"><?php _e('don\'t cache this widget?'); ?></label>
    	</p>
    	<?php
	}

and this will give us something like this in every widget:
Widget cache

So next we need to save that check-box and to do that we use the widget_update_callback filter hook, so:

	/**
	 * widget_update_callback
	 * @param array     $instance     The current widget instance's settings.
	 * @param array     $new_instance Array of new widget settings.
	 * @param array     $old_instance Array of old widget settings.
	 * @return array    $instance
	 */
	function widget_update_callback($instance, $new_instance, $old_instance){
		//save the checkbox if its set
		$instance['wc_cache'] = isset($new_instance['wc_cache']);
	    return $instance;
	}

And to hook these Two methods we update our constructor to:

	 /**
	 * __construct 
	 * 
	 * Class constructor where we will call our filter and action hooks.
	 */
	function __construct(){
		add_filter( 'widget_display_callback', array($this,'_cache_widget_output'), 10, 3 );
		add_action('in_widget_form', array($this,'in_widget_form'),5,3);
		add_filter('widget_update_callback', array($this,'widget_update_callback'),5,3);
	}

So now that we have our checkbox in place and we save it to the widget instance we can modify our _cache_widget_output method to check if the widgets needs to be cached or not so:

	/**
	 * _cache_widget_output 
	 * @param array     $instance The current widget instance's settings.
	 * @param WP_Widget $widget     The current widget instance.
	 * @param array     $args     An array of default widget arguments.
	 * @return mixed array|boolean
	 */
	function _cache_widget_output($instance, $widget, $args){
		if ( false === $instance )
			return $instance;

		//check if we need to cache this widget?
		if(isset($instance['wc_cache']) && $instance['wc_cache'] == true)
			return $instance;

		//simple timer to clock the widget rendring
		$timer_start = microtime(true);
		
		//create a uniqe transient ID for this widget instance
		$transient_name = $this->get_widget_key($instance,$args);

		//get the "cached version of the widget"
		if ( false === ( $cached_widget = get_transient( $transient_name ) ) ){
			// It wasn't there, so render the widget and save it as a transient
			// start a buffer to capture the widget output
			ob_start();
			//this renders the widget
			$widget->widget( $args, $instance );
			//get rendered widget from buffer
			$cached_widget = ob_get_clean();
			//save/cache the widget output as a transient 
			set_transient( $transient_name, $cached_widget, $this->cache_time);
		}

		//output the widget
		echo $cached_widget;
		//output rendering time as an html comment
		echo '<!-- From widget cache in '.number_format( microtime(true) - $timer_start, 5 ).' seconds -->';

		//after the widget was rendered and printed we return false to short-circuit the normal display of the widget 
		return false;	
	}

And putting it all together will give us:

<?php
/*
Plugin Name: GWP Widget Cache (Cached!)
Plugin URI: 
Description: A plugin to cache WordPress Widgets using the Transients API, based on this tutorial https://generatewp.com/?p=10132
Version: 1.0
Author: Ohad Raz
Author URI: https://generatewp.com
/**
* GWP_Widget_cache
*/
class GWP_Widget_cache{
	/**
	 * $cache_time 
	 * transient exiration time
	 * @var int
	 */
	public $cache_time = 43200; // 12 hours in seconds
	
	/**
	 * __construct 
	 * 
	 * Class constructor where we will call our filter and action hooks.
	 */
	function __construct(){
		add_filter( 'widget_display_callback', array($this,'_cache_widget_output'), 10, 3 );
		add_action('in_widget_form', array($this,'in_widget_form'),5,3);
		add_filter('widget_update_callback', array($this,'widget_update_callback'),5,3);
	}
	
	/**
	 * get_widget_key 
	 * 
	 * Simple function to generate a unique id for the widget transient
	 * based on the widget's instance and arguments
	 * 
	 * @param  array $i widget instance
	 * @param  array $a widget arguments
	 * @return string md5 hash
	 */
	function get_widget_key($i,$a){
		return 'WC-' . md5( serialize( array( $i, $a ) ) );
	}

	/**
	 * _cache_widget_output 
	 * @param array     $instance The current widget instance's settings.
	 * @param WP_Widget $widget     The current widget instance.
	 * @param array     $args     An array of default widget arguments.
	 * @return mixed array|boolean
	 */
	function _cache_widget_output($instance, $widget, $args){
		if ( false === $instance )
			return $instance;

		//check if we need to cache this widget?
		if(isset($instance['wc_cache']) && $instance['wc_cache'] == true)
			return $instance;

		//simple timer to clock the widget rendring
		$timer_start = microtime(true);
		
		//create a uniqe transient ID for this widget instance
		$transient_name = $this->get_widget_key($instance,$args);

		//get the "cached version of the widget"
		if ( false === ( $cached_widget = get_transient( $transient_name ) ) ){
			// It wasn't there, so render the widget and save it as a transient
			// start a buffer to capture the widget output
			ob_start();
			//this renders the widget
			$widget->widget( $args, $instance );
			//get rendered widget from buffer
			$cached_widget = ob_get_clean();
			//save/cache the widget output as a transient 
			set_transient( $transient_name, $cached_widget, $this->cache_time);
		}

		//output the widget
		echo $cached_widget;
		//output rendering time as an html comment
		echo '<!-- From widget cache in '.number_format( microtime(true) - $timer_start, 5 ).' seconds -->';

		//after the widget was rendered and printed we return false to short-circuit the normal display of the widget 
		return false;	
	}

	/**
	 * in_widget_form
	 * this method displays a checkbox in the widget panel
	 * 
	 * @param WP_Widget $t     The widget instance, passed by reference.
	 * @param null      $return   Return null if new fields are added.
     * @param array     $instance An array of the widget's settings.
	 * 
	 */
	function in_widget_form($t,$return,$instance){
		$instance = wp_parse_args( 
			(array) $instance, 
			array( 
				'title' => '', 
				'text' => '', 
				'wc_cache' => null
			)
		);

		if ( !isset($instance['wc_cache']) )
        	$instance['wc_cache'] = null;
        ?>
        <p>
        	<input id="<?php echo $t->get_field_id('wc_cache'); ?>" name="<?php echo $t->get_field_name('wc_cache'); ?>" type="checkbox" <?php checked(isset($instance['wc_cache']) ? $instance['wc_cache'] : 0); ?> />
        	<label for="<?php echo $t->get_field_id('wc_cache'); ?>"><?php _e('don\'t cache this widget?'); ?></label>
    	</p>
    	<?php
	}

	/**
	 * widget_update_callback
	 * @param array     $instance     The current widget instance's settings.
	 * @param array     $new_instance Array of new widget settings.
	 * @param array     $old_instance Array of old widget settings.
	 * @return array    $instance
	 */
	function widget_update_callback($instance, $new_instance, $old_instance){
		//save the checkbox if its set
		$instance['wc_cache'] = isset($new_instance['wc_cache']);
	    return $instance;
	}
}//end GWP_Widget_cache class


//initiate the class
add_action( 'plugins_loaded', 'GWP_Widget_cache_init' );
function GWP_Widget_cache_init() {
    $GLOBALS['GWP_Widget_cache'] = new GWP_Widget_cache();
}

Conclusion

We haven’t done nothing too complex and we got some performance gain.
The key is to know when and where to use Transients API but its very pain and simple to use.

Side note:

Some caching plugins speed up transients even more by using 3rd party software. For a example a memcached plugin will tell WordPress to store the transients values in memory instead of the database for even faster access and to avoid database calls on each page request.

Mainly for that reason

Transients should also never be assumed to be in the database, since they may not be stored there at all.

Do you have a better solution or are you getting better results? Let me know in the comments below.

Share on facebook
Share on twitter
Share on linkedin
Share on reddit
Share on whatsapp
Share on email
Ohad Raz

Ohad Raz

A WordPress Consultant, a plugins and themes developer,WordPress Development moderator and sometimes a WordPress core contributor.

13 Comments:

  • Jincheng

    Hi, could I directly put these codes into my theme’s functions.php?

    Reply

  • Chad Schulz

    I’ve temporarily deleted this plugin as I can’t seem to find the location of the transients anywhere in the DB or on the server. The expiration didn’t work and the widgets never refreshed until I deactivated the plugin.

    Otherwise, the performance increase was huge. I really appreciate the effort. But, I don’t want to create a site/db filled with orphaned transients that can’t be cleaned and don’t expire.

    Thanks, Chad

    Reply

    • Ohad Raz

      That is weird because the plugin uses get_transient to display the widget which validate’s the “freshness” of the data and if it has expired the data (the transient) gets deleted and that means that the DB clean up is done for you. (look at the function definition). So the only reasons I can think of for you not finding the location of the transients in the DB are:

      • You have external object cache enabled, which means that the transients are not stored in the database
      • You are simply not looking for the transients correctly, ex: if you have a transient named my_key then in the DB its stored with a _transient_ prefix so you get _transient_my_key and that is stored in the options table of the DB.

      If you do know what to look for and still can’t find it I’ll be happy to help you investigate.

      Reply

      • Chad Schulz

        Sorry, I forgot that I had made this post.

        It turns out that I’m using external object caching as you suggested and the transients are being stored there.

        My bad.

        This plugin is awesome! I’m currently using it on multiple websites to great benefit.

        Greatly appreciate both the plugin and the response to my query.
        Ohad

        Reply

  • bhumishah

    Awesome!!!! Sounds good.

    Reply

  • Christian Nikkanen

    Hey this is awesome!

    I will reverse the checkbox functionality so that it will require checking a checkbox to cache the widget and implementing an option page of some sort that will allow us to change the timeout values per site and delete transients with a click of a button.

    Thanks for this, this will save a lot of my time.

    Reply

  • Blaise

    Great work! Works straight out of the box and leverages my implementations of memcache and / or Redis!
    How is expiration of transients managed? If the widget changes some of its content, how are the transients expired?

    Keep me posted of any updates: I’ve subscribed to this thread!

    Good work,

    Blaise

    Reply

    • Ohad Raz

      WordPress manages the expiation of the transients for you, meaning if you try to get an expired transient you will get “FALSE”.
      As for updating the transient once the widget changes you would have to implement some kind of purging yourself.

      Reply

  • fauzievolute

    to check if in wp customizer $widget->is_preview(), skip cache then.

    Reply

  • niels

    Just wanted to say that im testdriving your plugin at the moment on a “membership” wordpress website.
    And im pretty amazed with the results; reduced the query count with 50%!
    Thanks a lot mate!

    Reply

Leave a Reply

Subscribe to our Blog

Receive email notifications when new blog posts are published.