Caching and Scaling With Fragment Caching

Erick Hitter (@ethitter)



Lead WordPress Developer at Oomph, Inc.
WordPress 3.3 Core Contributor
WordCamp Boston Organizer
Plugin Author

What is Caching?

Fragment Caching Benefits

Allow dynamic and static content to coexist

Fragment Caching Benefits

Common elements can be reused throughout a site


Fragment Caching Benefits

Reduce calls to APIs

WordPress' Native Caching APIs

Transients

Object Cache

Fragment Caching Basics: Creating

<?php
	function generate_cached_output() {
		if ( false === ( $output = wp_cache_get( $cache_key, $cache_group ) ) ) {
			$output = 'Something to be cached';

			wp_cache_set( $cache_key, $output, $cache_group, 86400 );
		}

		return $output;
	}
?>

Fragment Caching Basics: Clearing

<?php
	function clear_cached_output( $new_status, $old_status ) {
		if ( 'publish' == $new_status || 'publish' == $old_status )
			wp_cache_delete( $cache_key, $cache_group );
	}
	add_action( 'transition_post_status', 'clear_cached_output', 10, 2 );
?>

This above example clears a cache when anything is published or something that is published is modified. The "something" could be a post, page, or custom post type object.

If, instead, the cache should be rebuilt only when posts are edited, one additional argument from transition_post_status can be employed.

<?php
	function clear_cached_output( $new_status, $old_status, $post ) {
		if ( ( 'publish' == $new_status || 'publish' == $old_status ) && 'post' == $post->post_type )
			wp_cache_delete( $cache_key, $cache_group );
	}
	add_action( 'transition_post_status', 'clear_cached_output', 10, 3 );
?>

Fragment Caching Basics: Clearing

Same cache generation function from two slides ago, with a minor change

<?php
	function generate_cached_output( $force_refresh = false ) {
		if ( $force_refresh || false === ( $output = wp_cache_get( $cache_key, $cache_group ) ) ) {
			$output = 'Something to be cached';

			wp_cache_set( $cache_key, $output, $cache_group, 86400 );
		}

		return $output;
	}
?>

Clear by rebuilding cache

<?php
	function clear_cached_output( $new_status, $old_status ) {
		if ( 'publish' == $new_status || 'publish' == $old_status )
			generate_cached_output( true );
	}
	add_action( 'transition_post_status', 'clear_cached_output', 10, 2 );
?>

Unpredictable Keys

Unpredictable Keys: Recent Posts

<?php
	function recent_posts( $post_id = false, $qty = 3 ) {
		$post_id = (int) $post_id;
		if ( ! $post_id )
			return false;

		$qty = (int) $qty ? (int) $qty : 3;

		$cache_key = $post_id . '_' . $qty;

		if ( false === ( $output = wp_cache_get( $cache_key, 'recent_posts' ) ) ) {
			$output = 'Something to be cached';

			wp_cache_set( $cache_key, $output, 'recent_posts', 86400 );
		}

		return $output;
	}
?>

Unpredictable Keys: Cached Array

<?php
	function recent_posts( $post_id = false, $qty = 3 ) {
		/* Sanitize function arguments */

		$cache_key = $post_id . '_' . $qty;

		$cache = wp_cache_get( 'single', 'recent_posts' );
		if( ! is_array( $cache ) )
			$cache = array();

		if ( ! array_key_exists( $cache_key, $cache ) ) {
			$output = 'Something to be cached';

			$cache[ $cache_key ] = $output;
			wp_cache_set( 'single', $cache, 'recent_posts', 86400 );
		}

		return $output;
	}
?>

Unpredictable Keys: Cached Array

Pros

Cons

Unpredictable Keys: Incrementor

<?php
	function get_cache_incrementor() {
		$incrementor = wp_cache_get( 'incrementor', 'recent_posts' );
		if ( ! is_numeric( $incrementor ) ) {
			$incrementor = time();
			wp_cache_set( 'incrementor', $incrementor, 'recent_posts', 86400 );
		}

		return $incrementor;
	}

	function recent_posts( $post_id = false, $qty = 3 ) {
		/* Sanitize function arguments */

		$cache_key = get_cache_incrementor() . '_' . $post_id . '_' . $qty;

		if ( false === ( $output = wp_cache_get( $cache_key, 'recent_posts' ) ) ) {
			$output = 'Something to be cached';

			wp_cache_set( $cache_key, $output, 'recent_posts', 86400 );
		}

		return $output;
	}
?>

Where We Use Fragment Caching

category__not_in vs post__not_in

<?php
	new WP_Query( array(
		'category__not_in' => 167
	) );
?>
SELECT … WHERE 1=1  AND wp_posts.ID NOT IN ( SELECT tr.object_id FROM wp_term_relationships AS tr INNER JOIN wp_term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy = 'category' AND tt.term_id IN ('167') ) …

category__not_in vs post__not_in

<?php
	function cached_get_objects_in_term( $term_ids, $taxonomies, $args ) {
		/* Sanitize function arguments */

		$cache_key = md5( implode( ',', $term_ids ) . $taxonomies . serialize( $args ) );

		if ( false === ( $ids = wp_cache_get( $cache_key, 'get_objects_in_term' ) ) ) {
			$ids = get_objects_in_term( $term_ids, $taxonomies, $args );
			/* Error check $ids */
			wp_cache_set( $cache_key, $ids, 'get_objects_in_term', 86400 );
		}

		return $ids;
	}
?>

category__not_in vs post__not_in

<?php
	new WP_Query( array(
		'post__not_in' => cached_get_objects_in_term( 167, 'category' )
	) );
?>

Before

SELECT … WHERE 1=1  AND wp_posts.ID NOT IN ( SELECT tr.object_id FROM wp_term_relationships AS tr INNER JOIN wp_term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy = 'category' AND tt.term_id IN ('167') ) …

After

SELECT … WHERE 1=1  AND wp_posts.ID NOT IN ( '1','2','3','4','5' ) …

Menu Caching: No Active States

<?php
	function cache_wp_nav_menu( $args = false, $skip_cache = false ) {
		/* Sanitize function arguments */

		$echo = (bool) $args[ 'echo' ];
		$args[ 'echo' ] = false;

		$cache_key = md5( implode( '|', $args ) );

		if( $skip_cache || false === ( $menu = wp_cache_get( $cache_key, $this->cache_group ) ) ) {
			$menu = wp_nav_menu( $args );

			wp_cache_set( $cache_key, $menu, $this->cache_group, 86400 );
		}

		if( $echo )
			echo $menu;
		else
			return $menu;
	}
?>

Menu Caching: Active States

<?php
	function cache_wp_nav_menu( $args = false, $skip_cache = false ) {
		/* Sanitize function arguments */

		$echo = (bool) $args[ 'echo' ];
		$args[ 'echo' ] = false;

		$queried_object = (int) get_queried_object_id();
		if ( ! $queried_object )
			$queried_object = 0;

		$cache_key = get_cache_incrementor();
		$cache_key .= $queried_object . '|';
		$cache_key .= implode( '|', $args );
		$cache_key = md5( $cache_key );

		...
?>

Menu Caching: Keys & Clearing

query_posts() vs pre_get_posts

query_posts()

<?php
	//In home.php
	query_posts( array(
		'posts_per_page' => 5
	) );
?>

pre_get_posts

<?php
	function action_pre_get_posts( $query ) {
		if ( $query->is_home() )
			$query->set( 'posts_per_page', 5 );
	}
	add_action( 'pre_get_posts', 'action_pre_get_posts' );
?>

How Does This Factor Into Our Work On WordPress.com VIP?

Want to know more about WordPress.com infrastructure? Check out http://goo.gl/lYpJH.

Questions?

Email:

On Twitter: @ethitter

Slides will be available at http://www.ethitter.com/.