Tag: transients

  • Better Caching in WordPress

    Better Caching in WordPress

    Caching data in WordPress is easy. Caching data in WordPress in a good and performant way takes a bit more work. For instance, many developers commonly use the Transients API to cache data. As the lowest common denominator in caching, this is okay. It’ll get the job done, even on a $10/year shared hosting plan. But what we should do instead is leverage the WP_Object_Cache functions to provide more functionality and better features.

    For instance, let’s say we want to cache the result of an external API request. One way we could do this would be this way:

    Note: These examples are terrible, but I hope they get the point across!

    function get_api( $value) {
    	$value = absint( $value );
    	$api_data = get_transient( 'example-api-data-' . $value );
    
    	if ( false === $api_data ) {
    		$api_data = file_get_contents( 'https://example.com/api/' . $value );
    		set_transient( 'example-api-data-' . $value, $api_data, HOUR_IN_SECONDS * 6 );
    	}
    
    	return json_decode( $api_data );
    }Code language: PHP (php)

    What’s one way we could make this better by using the WP_Object_Cache functions? Well, what happens if the API data structure changes, and you need to invalidate every cache value? It would be pretty hard to know the exact transient keys that you’d need to clear, and clearing the entire cache is a bit too nuclear for this (but it would work). Instead, wp_cache_*() could be used, which includes the ability to use a cache group that can be changed:

    function get_api( $value) {
    	$value = absint( $value );
    	$cache_group = 'example-api-data';
    
    	$api_data = wp_cache_get( $value, $cache_group );
    
    	if ( false === $api_data ) {
    		$api_data = file_get_contents( 'https://example.com/api/' . $value );
    		wp_cache_set( $value, $api_data, $cache_group, HOUR_IN_SECONDS * 6 );
    	}
    
    	return json_decode( $api_data );
    }Code language: PHP (php)

    With this, if we ever need to invalidate the cache for this API, we just need to change the $cache_group value, and all cache requests will be new.


    Another common theme I see is caching too much data. Let’s say you’re going to do a slow WP_Query, and want to cache the results for better performance:

    function get_new_posts() {
    	$posts = wp_cache_get( 'new-posts' );
    
    	if ( false === $posts ) {
    		$posts = new WP_Query( 'posts_per_page=5000' );
    		wp_cache_set( 'new-posts', $posts );
    	}
    
    	return $posts;
    }Code language: PHP (php)

    Sure, that’s fine and it’ll work but… the WP_Query object is huge!

    echo strlen( serialize( new WP_Query( 'posts_per_page=500' ) ) ); … 2,430,748

    That’s 2.5 megs of data needing to be transferred out of cache on every pageload. If your cache is accessed across the network on another server, this introduces more delay as it has to transfer. Also, some caching solutions might put a limit on the size of an individual cache object–which means that an object like this might never be cached!

    Instead, we can just grab the IDs of the posts, and do a second, much faster query:

    function get_new_posts() {
    	$post_ids = wp_cache_get( 'new-posts' );
    
    	if ( false === $posts ) {
    		$post_ids = new WP_Query( 'posts_per_page=5000&fields=ids' );
    		wp_cache_set( 'new-posts', $posts->posts );
    	}
    
    	$posts = new WP_Query( [ 'post__in' => $post_ids ] );
    
    	return $posts;
    }Code language: PHP (php)

    echo strlen( serialize( $posts->posts ) ); … only 88,838 bytes, that’s like a 96%-something difference!

    I had a few more ideas for this post, but it’s been sitting as a draft forever and I don’t remember. It’s possible this topic might be revisited some day 🙂

  • Purging All The Caches!

    Purging All The Caches!

    One of the best ways to ensure that a WordPress site–well any site really–stays performant and not broken is by leveraging caching.

    WordPress by default doesn’t do much caching other than some in-memory caching of objects, and the odd database caching via the Transients API.

    This site currently has three layers of caching:

    This means I have three different plugins that I have to manage with these caches:

    So if I am doing some development and want to purge one or more caches, I need to go dig around three different places to purge these, and that’s not fun.  To help combat this, I made myself a simple Admin Dashboard widget with quick access to purge each of these:

    Here’s the code:

    <?php
    class Emrikol_Cache_Dashboard {
    	public static function instance() {
    		static $instance = false;
    		if ( ! $instance ) {
    			$instance = new Emrikol_Cache_Dashboard();
    		}
    		return $instance;
    	}
    
    	public function __construct() {
    		add_action( 'init', array( $this, 'init' ) );
    	}
    
    	public function init() {
    		if ( $this->is_admin() && isset( $_GET['ead_purge_object_cache'] ) && check_admin_referer( 'manual_purge' ) ) {
    			$did_flush = wp_cache_flush();
    			if ( $did_flush ) {
    				add_action( 'admin_notices', array( $this, 'message_object_cache_purge_success' ) );
    			} else {
    				add_action( 'admin_notices', array( $this, 'message_object_cache_purge_failure' ) );
    			}
    		} elseif ( $this->is_admin() && isset( $_GET['ead_purge_wp_super_cache'] ) && check_admin_referer( 'manual_purge' ) ) {
    			global $file_prefix;
    			wp_cache_clean_cache( $file_prefix, true );
    			add_action( 'admin_notices', array( $this, 'message_wp_super_cache_purge_success' ) );
    		} elseif ( $this->is_admin() && isset( $_GET['ead_purge_OPcache'] ) && check_admin_referer( 'manual_purge' ) ) {
    			// Taken from: https://wordpress.org/plugins/flush-opcache/
    			// Check if file cache is enabled and delete it if enabled.
    			// phpcs:ignore WordPress.VIP.FileSystemWritesDisallow.file_ops_is_writable
    			if ( ini_get( 'OPcache.file_cache' ) && is_writable( ini_get( 'OPcache.file_cache' ) ) ) {
    				$files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( ini_get( 'OPcache.file_cache' ), RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST );
    				foreach ( $files as $fileinfo ) {
    					$todo = ( $fileinfo->isDir() ? 'rmdir' : 'unlink' );
    					$todo( $fileinfo->getRealPath() );
    				}
    			}
    
    			// Flush OPcache.
    			$did_flush = OPcache_reset();
    			if ( $did_flush ) {
    				add_action( 'admin_notices', array( $this, 'message_OPcache_purge_success' ) );
    			} else {
    				add_action( 'admin_notices', array( $this, 'message_OPcache_purge_failure' ) );
    			}
    		}
    
    		add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widgets' ) );
    	}
    
    	public function add_dashboard_widgets() {
    		if ( $this->is_admin() ) {
    			wp_add_dashboard_widget( 'emrikol_admin_dashboard', 'Cache Control', array( $this, 'show_admin_dashboard' ) );
    		}
    	}
    
    	public function show_admin_dashboard() {
    		if ( false === get_parent_class( $GLOBALS['wp_object_cache'] ) ) {
    			// Persistent Object Cache detected.
    			?>
    			<div class="activity-block">
    				<span class="button"><a href="<?php echo esc_url( wp_nonce_url( admin_url( '?ead_purge_object_cache' ), 'manual_purge' ) ); ?>"><strong>Purge Object Cache</strong></a></span>
    				<p>Force a purge of your entire site's object cache.</p>
    			</div>
    			<?php
    		} else {
    			// Transients!
    			?>
    			<div class="activity-block">
    				<h3>Transients</h3>
    				<p>Transients cannot currently be removed manually.</p>
    			</div>
    			<?php
    		}
    		if ( function_exists( 'wp_cache_clean_cache' ) ) {
    			// WP Super Cache!
    			?>
    			<div class="activity-block">
    				<span class="button"><a href="<?php echo esc_url( wp_nonce_url( admin_url( '?ead_purge_wp_super_cache' ), 'manual_purge' ) ); ?>"><strong>Purge Page Cache</strong></a></span>
    				<p>Force a purge of your entire site's page cache.</p>
    			</div>
    			<?php
    		}
    		if ( function_exists( 'OPcache_reset' ) ) {
    			// PHP OPcache.
    			?>
    			<div class="activity-block">
    				<span class="button"><a href="<?php echo esc_url( wp_nonce_url( admin_url( '?ead_purge_OPcache' ), 'manual_purge' ) ); ?>"><strong>Purge PHP OPcache</strong></a></span>
    				<p>Force a purge of your entire site's PHP OPcache.</p>
    			</div>
    			<?php
    		}
    	}
    
    	public function message_wp_super_cache_purge_success() {
    		echo '<div id="message" class="notice notice-success is-dismissible"><p>Page Cache purged!</p></div>';
    	}
    
    	public function message_object_cache_purge_success() {
    		echo '<div id="message" class="notice notice-success is-dismissible"><p>Object Cache purged!</p></div>';
    	}
    
    	public function message_object_cache_purge_failure() {
    		echo '<div id="message" class="notice notice-error is-dismissible"><p>Object Cache purge failed!</p></div>';
    	}
    
    	public function message_OPcache_purge_success() {
    		echo '<div id="message" class="notice notice-success is-dismissible"><p>PHP OPcache purged!</p></div>';
    	}
    
    	public function message_OPcache_purge_failure() {
    		echo '<div id="message" class="notice notice-error is-dismissible"><p>PHP OPcache purge failed!</p></div>';
    	}
    
    	private function is_admin() {
    		if ( current_user_can( 'manage_options' ) ) {
    			return true;
    		}
    		return false;
    	}
    }
    Emrikol_Cache_Dashboard::instance();
    Code language: HTML, XML (xml)