20 WordPress Core Functions That Don’t Scale and How to Work Around Them

WordPress is currently used by over 30% of all websites and 59% of all content management systems. One of the major concerns that people have when it comes to WordPress is the speed! With the release of PHP 7, some of those speed concerns have been fixed.

There are other solutions that WordPress developers use to get around the speed issue such as using a full-page caching plugin, using a CDN among others. There’s, however, nothing like improving the raw speed of uncached pages especially when you need almost real-time updates with the service.This is particularly useful when you are using WordPress to build something other than a static content distribution website where the post don’t change frequently.

WordPress comes with a set of core functions that are globally accessible. Most theme and plugin developers rely on these function to extend the functionality of the WordPress framework. There are however a collection of functions that are not necessarily fast when used repeatedly, in other words, they do not scale.

Fortunately, Automattic, the company behind the paid VIP WordPress service, has released a set of functions that do not scale properly because they are not cached and possible workarounds around those functions.

WordPress core has a number of functions that, for various reasons, are uncached, which means that calling them will always result in an SQL query. Below, we outline some of these functions. We have a helper file with cached versions of some of the functions, which is automatically available to you.

  • get_posts()
    • Unlike WP_Query, the results of get_posts() are not cached via Advanced Post Cache used on the WordPress.com VIP platform.
    • Use WP_Query instead, or set 'suppress_filters' => false.
    • When using WP_Query instead of get_posts don’t forget about setting ignore_sticky_posts and no_found_rows params appropriately (both are hardcoded inside a get_posts function with value of true )
  • wp_get_recent_posts()
    • See get_posts()
  • get_children()
    • Similar to get_posts(), but also performs a no-LIMIT query among other bad things by default. Alias of break_my_site_now_please(). Do not use. Instead do a regular WP_Query and make sure that the post_parent you are looking for is not 0 or a falsey value. Also make sure to set a reasonable posts_per_page, get_children will do a -1 query by default, a maximum of 100 should be used (but a smaller value could increase performance)
  • term_exists()
    • Use wpcom_vip_term_exists() instead
  • get_term_link(), get_tag_link(), and get_category_link()
    • Use wpcom_vip_get_term_link() instead
  • get_page_by_title()
  • get_page_by_path()
    • Use wpcom_vip_get_page_by_path() instead
  • url_to_post_id() and url_to_postid()
    • Use wpcom_vip_url_to_postid() instead
  • count_user_posts()
    • Use wpcom_vip_count_user_posts() instead
  • wp_old_slug_redirect()
    • Use wpcom_vip_old_slug_redirect() instead.
  • get_adjacent_post()get_previous_post()get_next_post(), previous_post_link(), next_post_link()
    • Use  wpcom_vip_get_adjacent_post() instead.
  • attachment_url_to_postid()
    • Use  wpcom_vip_attachment_url_to_postid() instead.

Below are the function workarounds for the WordPress core functions.


<?php
/**
 * This file contains a bunch of helper functions that handle add caching to core WordPress functions.
 */

/**
 * Cached version of get_category_by_slug.
 *
 * @param string $slug Category slug
 * @return object|null|bool Term Row from database. Will return null if $slug doesn't match a term. If taxonomy does not exist then false will be returned.
 * @link http://vip.wordpress.com/documentation/uncached-functions/ Uncached Functions
 */
function wpcom_vip_get_category_by_slug( $slug ) {
	return wpcom_vip_get_term_by( 'slug', $slug, 'category' );
}

/**
 * Cached version of get_term_by.
 *
 * Many calls to get_term_by (with name or slug lookup) across on a single pageload can easily add up the query count.
 * This function helps prevent that by adding a layer of caching.
 *
 * @param string $field Either 'slug', 'name', or 'id'
 * @param string|int $value Search for this term value
 * @param string $taxonomy Taxonomy Name
 * @param string $output Optional. Constant OBJECT, ARRAY_A, or ARRAY_N
 * @param string $filter Optional. Default is 'raw' or no WordPress defined filter will applied.
 * @return mixed|null|bool Term Row from database in the type specified by $filter. Will return false if $taxonomy does not exist or $term was not found.
 * @link http://vip.wordpress.com/documentation/uncached-functions/ Uncached Functions
 */
function wpcom_vip_get_term_by( $field, $value, $taxonomy, $output = OBJECT, $filter = 'raw' ) {
	// ID lookups are cached
	if ( 'id' == $field )
		return get_term_by( $field, $value, $taxonomy, $output, $filter );

	$cache_key = $field . '|' . $taxonomy . '|' . md5( $value );
	$term_id = wp_cache_get( $cache_key, 'get_term_by' );

	if ( false === $term_id ) {
		$term = get_term_by( $field, $value, $taxonomy );

		if ( $term && ! is_wp_error( $term ) ) {
			wp_cache_set( $cache_key, $term->term_id, 'get_term_by', 4 * HOUR_IN_SECONDS );
		} else {
			wp_cache_set( $cache_key, 0, 'get_term_by', 15 * MINUTE_IN_SECONDS ); // if we get an invalid value, let's cache it anyway but for a shorter period of time
		}

		if ( OBJECT !== $output && $term && ! is_wp_error( $term ) ) {  //Previously we'd ignore the $output parameter the first time this function was called, always returning an object, this lead to unexpected behaviour. This line could go in the previous conditional a few lines up but it feels more readable and logical to separate it from the caching conditional
			$term = get_term( $term->term_id, $taxonomy, $output, $filter );
		}

	} else {
		$term = get_term( $term_id, $taxonomy, $output, $filter );
	}

	if ( is_wp_error( $term ) )
		$term = false;

	return $term;
}

/**
 * Properly clear wpcom_vip_get_term_by() cache when a term is updated
 */
add_action( 'edit_terms', 'wp_flush_get_term_by_cache', 10, 2 );
add_action( 'create_term', 'wp_flush_get_term_by_cache', 10, 2 );
function wp_flush_get_term_by_cache( $term_id, $taxonomy ){
	$term = get_term_by( 'id', $term_id, $taxonomy );
	if ( ! $term ) {
		return;
	}
	foreach( array( 'name', 'slug' ) as $field ) {
		$cache_key = $field . '|' . $taxonomy . '|' . md5( $term->$field );
		$cache_group = 'get_term_by';
		wp_cache_delete( $cache_key, $cache_group );
	}
}

/**
 * Cached version of term_exists()
 *
 * Term exists calls can pile up on a single pageload.
 * This function adds a layer of caching to prevent lots of queries.
 *
 * @param int|string $term The term to check can be id, slug or name.
 * @param string $taxonomy The taxonomy name to use
 * @param int $parent Optional. ID of parent term under which to confine the exists search.
 * @return mixed Returns null if the term does not exist. Returns the term ID
 *               if no taxonomy is specified and the term ID exists. Returns
 *               an array of the term ID and the term taxonomy ID the taxonomy
 *               is specified and the pairing exists.
 */

function wpcom_vip_term_exists( $term, $taxonomy = '', $parent = null ) {
	//If $parent is not null, let's skip the cache.
	if ( null !== $parent ){
		return term_exists( $term, $taxonomy, $parent );
	}

	if ( ! empty( $taxonomy ) ){
		$cache_key = $term . '|' . $taxonomy;
	}else{
		$cache_key = $term;
	}

	$cache_value = wp_cache_get( $cache_key, 'term_exists' );

	//term_exists frequently returns null, but (happily) never false
	if ( false  === $cache_value ) {
		$term_exists = term_exists( $term, $taxonomy );
		wp_cache_set( $cache_key, $term_exists, 'term_exists', 3 * HOUR_IN_SECONDS );
	}else{
		$term_exists = $cache_value;
	}

	if ( is_wp_error( $term_exists ) )
		$term_exists = null;

	return $term_exists;
}

/**
 * Properly clear wpcom_vip_term_exists() cache when a term is updated
 */
add_action( 'delete_term', 'wp_flush_term_exists', 10, 4 );
function wp_flush_term_exists( $term, $tt_id, $taxonomy, $deleted_term ){
	foreach( array( 'term_id', 'name', 'slug' ) as $field ) {
		$cache_key = $deleted_term->$field . '|' . $taxonomy ;
		$cache_group = 'term_exists';
		wp_cache_delete( $cache_key, $cache_group );
	}
}


/**
 * When a term is split we need to delete the caches we have put in place.
 */
add_action( 'split_shared_term', function( $term_id, $new_term_id, $term_taxonomy_id, $taxonomy ) {
	$term = wpcom_vip_get_term_by( 'id', $new_term_id, $taxonomy );
	/**
	 * When 'id' is passed as the $field to wpcom_vip_get_term_by(),
	 * get_term_by() is used bypassing this caching
	 */
	foreach ( array( 'slug', 'name', 'term_taxonomy_id' ) as $field ) {
		$cache_key = $field . '|' . $taxonomy . '|' . md5( $term->$field );
		wp_cache_delete( $cache_key, 'get_term_by' );
	}
	/**
	 * While the term still exists after being split, term_exists returns the ID of the term which might be used in some circumstances and would now have potentially changed.
	 * The Taxonomy might not be part of the cache key so lets bust both.
	 */
	foreach ( array( 'term_id', 'name', 'slug' ) as $field ) {
		$cache_key = $term->$field . '|' . $taxonomy ;
		wp_cache_delete( $cache_key, 'term_exists' );
		$cache_key = $term->$field ;
		wp_cache_delete( $cache_key, 'term_exists' );
	}
	/**
	 * There's a period of time during term-splitting and before the cache clear happens that WP_Errors could be thrown as a result of stale cache data.
	 * Clear the taxonomy relationships cache to prevent this.
	 */
	$posts_per_page = 100;
	$paged = 1;

	do {
		$args = array(
			'posts_per_page' => $posts_per_page,
			'fields' => 'ids',
			'paged' => $paged,
			'tax_query' => array(
				array(
					'taxonomy' => $taxonomy,
					'field' => 'term_id',
					'terms' => $new_term_id,
				)
			)
		);

		$posts = get_posts( $args );

		foreach ( $posts as $post ) {
			wp_cache_delete( $post, $taxonomy . '_relationships' );
		}

		$paged++;
		sleep(2);
	} while ( count( $posts ) );
}, 10, 4 );


/**
 * Optimized version of get_term_link that adds caching for slug-based lookups.
 *
 * Returns permalink for a taxonomy term archive, or a WP_Error object if the term does not exist.
 *
 * @param int|string|object $term The term object / term ID / term slug whose link will be retrieved.
 * @param string $taxonomy The taxonomy slug. NOT required if you pass the term object in the first parameter 
 *
 * @return string|WP_Error HTML link to taxonomy term archive on success, WP_Error if term does not exist. 
 */
function wpcom_vip_get_term_link( $term, $taxonomy = null ) {
	// ID- or object-based lookups already result in cached lookups, so we can ignore those.
	if ( is_numeric( $term ) || is_object( $term ) ) {
		return get_term_link( $term, $taxonomy );
	}

	$term_object = wpcom_vip_get_term_by( 'slug', $term, $taxonomy );
	return get_term_link( $term_object );
}

/**
 * Cached version of get_page_by_title so that we're not making unnecessary SQL all the time
 *
 * @param string $page_title Page title
 * @param string $output Optional. Output type; OBJECT*, ARRAY_N, or ARRAY_A.
 * @param string $post_type Optional. Post type; default is 'page'.
 * @return WP_Post|null WP_Post on success or null on failure
 * @link http://vip.wordpress.com/documentation/uncached-functions/ Uncached Functions
 */
function wpcom_vip_get_page_by_title( $title, $output = OBJECT, $post_type = 'page' ) {
	$cache_key = $post_type . '_' . sanitize_key( $title );
	$page_id = wp_cache_get( $cache_key, 'get_page_by_title' );

	if ( $page_id === false ) {
		$page = get_page_by_title( $title, OBJECT, $post_type );
		$page_id = $page ? $page->ID : 0;
		wp_cache_set( $cache_key, $page_id, 'get_page_by_title', 3 * HOUR_IN_SECONDS ); // We only store the ID to keep our footprint small
	}

	if ( $page_id )
		return get_page( $page_id, $output );

	return null;
}

/**
 * Cached version of get_page_by_path so that we're not making unnecessary SQL all the time
 *
 * @param string $page_path Page path
 * @param string $output Optional. Output type; OBJECT*, ARRAY_N, or ARRAY_A.
 * @param string $post_type Optional. Post type; default is 'page'.
 * @return WP_Post|null WP_Post on success or null on failure
 * @link http://vip.wordpress.com/documentation/uncached-functions/ Uncached Functions
 */
function wpcom_vip_get_page_by_path( $page_path, $output = OBJECT, $post_type = 'page' ) {
	if ( is_array( $post_type ) )
		$cache_key = sanitize_key( $page_path ) . '_' . md5( serialize( $post_type ) );
	else
		$cache_key = $post_type . '_' . sanitize_key( $page_path );

	$page_id = wp_cache_get( $cache_key, 'get_page_by_path' );

	if ( $page_id === false ) {
		$page = get_page_by_path( $page_path, $output, $post_type );
		$page_id = $page ? $page->ID : 0;
		if ( $page_id === 0 ){
			wp_cache_set( $cache_key, $page_id, 'get_page_by_path', ( 1 * HOUR_IN_SECONDS + mt_rand(0, HOUR_IN_SECONDS) )  ); // We only store the ID to keep our footprint small
		}else{
			wp_cache_set( $cache_key, $page_id, 'get_page_by_path', 0); // We only store the ID to keep our footprint small
		}
	}

	if ( $page_id )
		return get_page( $page_id, $output );

	return null;
}

/**
 * Flush the cache for published pages so we don't end up with stale data
 *
 * @param string $new_status The post's new status
 * @param string $old_status The post's previous status
 * @param WP_Post $post The post
 * @link http://vip.wordpress.com/documentation/uncached-functions/ Uncached Functions
 */
function wpcom_vip_flush_get_page_by_title_cache( $new_status, $old_status, $post ) {
	if ( 'publish' == $new_status || 'publish' == $old_status )
		wp_cache_delete( $post->post_type . '_' . sanitize_key( $post->post_title ), 'get_page_by_title' );
}
add_action( 'transition_post_status', 'wpcom_vip_flush_get_page_by_title_cache', 10, 3 );

/**
 * Flush the cache for published pages so we don't end up with stale data
 *
 * @param string  $new_status The post's new status
 * @param string  $old_status The post's previous status
 * @param WP_Post $post       The post
 *
 * @link http://vip.wordpress.com/documentation/uncached-functions/ Uncached Functions
 */
function wpcom_vip_flush_get_page_by_path_cache( $new_status, $old_status, $post ) {
	if ( 'publish' === $new_status || 'publish' === $old_status ) {
		$page_path = get_page_uri( $post->ID );
		wp_cache_delete( $post->post_type . '_' . sanitize_key( $page_path ), 'get_page_by_path' );
	}
}
add_action( 'transition_post_status', 'wpcom_vip_flush_get_page_by_path_cache', 10, 3 );

/**
 * Cached version of url_to_postid, which can be expensive.
 *
 * Examine a url and try to determine the post ID it represents.
 * 
 * @param string $url Permalink to check.
 * @return int Post ID, or 0 on failure.
 */
function wpcom_vip_url_to_postid( $url ) {
	// Can only run after init, since home_url() has not been filtered to the mapped domain prior to that,
	// which will cause url_to_postid to fail
	// @see https://vip.wordpress.com/documentation/vip-development-tips-tricks/home_url-vs-site_url/
	if ( ! did_action( 'init' ) ) {
		_doing_it_wrong( 'wpcom_vip_url_to_postid', 'wpcom_vip_url_to_postid must be called after the init action, as home_url() has not yet been filtered', '' );

		return 0;
	}

	// Sanity check; no URLs not from this site
	if ( parse_url( $url, PHP_URL_HOST ) != wpcom_vip_get_home_host() )
		return 0;

	$cache_key = md5( $url );
	$post_id = wp_cache_get( $cache_key, 'url_to_postid' );

	if ( false === $post_id ) {
		$post_id = url_to_postid( $url ); // returns 0 on failure, so need to catch the false condition
		wp_cache_set( $cache_key, $post_id, 'url_to_postid', 3 * HOUR_IN_SECONDS );
	}

	return $post_id;
}

add_action( 'transition_post_status', function( $new_status, $old_status, $post ) {
	if ( 'publish' != $new_status && 'publish' != $old_status )
		return;

	$url = get_permalink( $post->ID );
	wp_cache_delete( md5( $url ), 'url_to_postid' );
}, 10, 3 );

/**
 * Cached version of wp_old_slug_redirect.
 *
 * Cache the results of the _wp_old_slug meta query, which can be expensive.
 * @deprecated use wpcom_vip_wp_old_slug_redirect instead
 */
function wpcom_vip_old_slug_redirect() {
    global $wp_query;
    if ( is_404() && '' != $wp_query->query_vars['name'] ) :
        global $wpdb;

        // Guess the current post_type based on the query vars.
        if ( get_query_var('post_type') )
            $post_type = get_query_var('post_type');
        elseif ( !empty($wp_query->query_vars['pagename']) )
            $post_type = 'page';
        else
            $post_type = 'post';

        if ( is_array( $post_type ) ) {
            if ( count( $post_type ) > 1 )
                return;
            $post_type = array_shift( $post_type );
        }

        // Do not attempt redirect for hierarchical post types
        if ( is_post_type_hierarchical( $post_type ) )
            return;

        $query = $wpdb->prepare("SELECT post_id FROM $wpdb->postmeta, $wpdb->posts WHERE ID = post_id AND post_type = %s AND meta_key = '_wp_old_slug' AND meta_value = %s", $post_type, $wp_query->query_vars['name']);

        // if year, monthnum, or day have been specified, make our query more precise
        // just in case there are multiple identical _wp_old_slug values
        if ( '' != $wp_query->query_vars['year'] )
            $query .= $wpdb->prepare(" AND YEAR(post_date) = %d", $wp_query->query_vars['year']);
        if ( '' != $wp_query->query_vars['monthnum'] )
            $query .= $wpdb->prepare(" AND MONTH(post_date) = %d", $wp_query->query_vars['monthnum']);
        if ( '' != $wp_query->query_vars['day'] )
            $query .= $wpdb->prepare(" AND DAYOFMONTH(post_date) = %d", $wp_query->query_vars['day']);

        $cache_key = md5( serialize( $query ) );

        if ( false === $id = wp_cache_get( $cache_key, 'wp_old_slug_redirect' ) ) {
            $id = (int) $wpdb->get_var($query);

            wp_cache_set( $cache_key, $id, 'wp_old_slug_redirect', 5 * MINUTE_IN_SECONDS );
        }

        if ( ! $id )
            return;

        $link = get_permalink($id);

        if ( !$link )
            return;

        wp_redirect( $link, 301 ); // Permanent redirect
        exit;
    endif;
}


/**
 * Cached version of count_user_posts, which is uncached but doesn't always need to hit the db
 *
 * count_user_posts is generally fast, but it can be easy to end up with many redundant queries
 * if it's called several times per request. This allows bypassing the db queries in favor of 
 * the cache
 */
function wpcom_vip_count_user_posts( $user_id ) {
    if ( ! is_numeric( $user_id ) ) {
        return 0;
    }

    $cache_key = 'vip_' . (int) $user_id;
    $cache_group = 'user_posts_count';

    if ( false === ( $count = wp_cache_get( $cache_key, $cache_group ) ) ) {
        $count = count_user_posts( $user_id );

        wp_cache_set( $cache_key, $count, $cache_group, 5 * MINUTE_IN_SECONDS );
    }

    return $count;
}

/*
 * Cached version of wp_get_nav_menu_object
 *
 * Many calls to get_term_by (with name or slug lookup as used inside the wp_get_nav_menu_object) across on a single pageload can easily add up the query count.
 * This function helps prevent that by taking advantage of wpcom_vip_get_term_by function which adds a layer of caching.
 *
 * @param string $menu Menu ID, slug, or name.
 * @uses wpcom_vip_get_term_by
 * @return mixed false if $menu param isn't supplied or term does not exist, menu object if successful.
 */
function wpcom_vip_get_nav_menu_object( $menu ) {
	if ( ! $menu )
		return false;

	$menu_obj = get_term( $menu, 'nav_menu' );

	if ( ! $menu_obj ) {
		$menu_obj = wpcom_vip_get_term_by( 'slug', $menu, 'nav_menu' );
	}

	if ( ! $menu_obj ) {
		$menu_obj = wpcom_vip_get_term_by( 'name', $menu, 'nav_menu' );
	}

	if ( ! $menu_obj ) {
		$menu_obj = false;
	}

	return $menu_obj;
}

/**
 * Require the Stampedeless_Cache class for use in our helper functions below.
 *
 * The Stampedeless_Cache helps prevent cache stampedes by internally varying the cache
 * expiration slightly when creating a cache entry in an effort to avoid multiple keys
 * expiring simultaneously and allowing a single request to regenerate the cache shortly
 * before it's expiration.
 */
if( function_exists( 'require_lib' ) && defined( 'WPCOM_IS_VIP_ENV' ) && WPCOM_IS_VIP_ENV )
	require_lib( 'class.stampedeless-cache' );

/**
 * Drop in replacement for wp_cache_set().
 *
 * Wrapper for the WPCOM Stampedeless_Cache class.
 *
 * @param string $key Cache key.
 * @param string|int|array|object $value Data to store in the cache.
 * @param string $group Optional. Cache group.
 * @param int $expiration Optional. Cache TTL in seconds.
 * @return bool This function always returns true.
 */
function wpcom_vip_cache_set( $key, $value, $group = '', $expiration = 0 ) {
	if( ! class_exists( 'Stampedeless_Cache' ) )
		return wp_cache_set( $key, $value, $group, $expiration );

	$sc = new Stampedeless_Cache( $key, $group );
	$sc->set( $value, $expiration );

	return true;
}

/**
 * Drop in replacement for wp_cache_get().
 *
 * Wrapper for the WPCOM Stampedeless_Cache class.
 *
 * @param string $key Cache key.
 * @param string $group Optional. Cache group.
 * @return mixed Returns false if failing to retrieve cache entry or the cached data otherwise.
 */
function wpcom_vip_cache_get( $key, $group = '' ) {
	if( ! class_exists( 'Stampedeless_Cache' ) )
		return wp_cache_get( $key, $group );

	$sc = new Stampedeless_Cache( $key, $group );

	return $sc->get();
}

/**
 * Drop in replacement for wp_cache_delete().
 *
 * Wrapper for WPCOM Stampedeless_Cache class.
 *
 * @param string $key Cache key.
 * @param string $group Optional. Cache group.
 * @return bool True on successful removal, false on failure.
 */
function wpcom_vip_cache_delete( $key, $group = '' ) {
	//delete cache itself
	$deleted = wp_cache_delete( $key, $group );
	
	if ( class_exists( 'Stampedeless_Cache' ) ) {
		//delete lock
		$lock_key = $key . '_lock';
		wp_cache_delete( $lock_key, $group );
	}

	return $deleted;
}

/**
 * Retrieve adjacent post.
 *
 * Can either be next or previous post. The logic for excluding terms is handled within PHP, for performance benefits.
 * Props to Elliott Stocks
 *
 * @global wpdb $wpdb
 *
 * @param bool         $in_same_term   Optional. Whether post should be in a same taxonomy term. Note - only the first term will be used from wp_get_object_terms().
 * @param int 	       $excluded_term  Optional. The term to exclude.
 * @param bool         $previous       Optional. Whether to retrieve previous post.
 * @param string       $taxonomy       Optional. Taxonomy, if $in_same_term is true. Default 'category'.
 *
 * @return null|string|WP_Post Post object if successful. Null if global $post is not set. Empty string if no corresponding post exists.
 */
function wpcom_vip_get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previous = true, $taxonomy = 'category', $adjacent = '' ) {
	global $wpdb;
	if ( ( ! $post = get_post() ) || ! taxonomy_exists( $taxonomy ) ) {
		return null;
	}
	$join = '';
	$where = '';
	$current_post_date = $post->post_date;

	if ( $in_same_term ) {
		if ( is_object_in_taxonomy( $post->post_type, $taxonomy ) ) {
			$term_array = get_the_terms( $post->ID, $taxonomy );
			if ( ! empty( $term_array ) && ! is_wp_error( $term_array ) ) {
				$term_array_ids = wp_list_pluck( $term_array, 'term_id' );
				// Remove any exclusions from the term array to include.
				$excluded_terms = explode( ',', $excluded_terms );
				if ( ! empty( $excluded_terms ) ){
					$term_array_ids = array_diff( $term_array_ids, (array) $excluded_terms );
				}
				if ( ! empty ( $term_array_ids ) ){
					$term_array_ids = array_map( 'intval', $term_array_ids );
					$term_id_to_search = array_pop( $term_array_ids ); // only allow for a single term to be used. picked pseudo randomly
				}else{
					$term_id_to_search = false;
				}

				$term_id_to_search = apply_filters( 'wpcom_vip_limit_adjacent_post_term_id', $term_id_to_search, $term_array_ids, $excluded_terms, $taxonomy, $previous );

				if ( ! empty( $term_id_to_search ) ){ //allow filters to short circuit by returning a empty like value
					$join = " INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id INNER JOIN $wpdb->term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id"; //Only join if we are sure there is a term
					$where = $wpdb->prepare( "AND tt.taxonomy = %s AND tt.term_id IN (%d)  ", $taxonomy,$term_id_to_search ); //
				}
			}
		}
	}

	$op = $previous ? '<' : '>';
	$order = $previous ? 'DESC' : 'ASC';
	$limit = 1;
	// We need 5 posts so we can filter the excluded term later on
	if ( ! empty ( $excluded_term ) ) {
		$limit = 5;
	}
	$sort  = "ORDER BY p.post_date $order LIMIT $limit";
	$where = $wpdb->prepare( "WHERE p.post_date $op %s AND p.post_type = %s AND p.post_status = 'publish' $where", $current_post_date, $post->post_type );
	$query = "SELECT p.ID FROM $wpdb->posts AS p $join $where $sort";

	$found_post = ''; // blank instead of false so not found is cached.
	$query_key = 'wpcom_vip_adjacent_post_' . md5( $query );
	$cached_result = wp_cache_get( $query_key );

	if( "not found" === $cached_result){
		return false;
	} else if ( false !== $cached_result ) {
		return get_post( $cached_result );
	}

	if ( empty ( $excluded_term ) ) {
		$result = $wpdb->get_var( $query );
	} else {
		$result = $wpdb->get_results( $query );
	}

	// Find the first post which doesn't have an excluded term
	if ( ! empty ( $excluded_term ) ) {
		foreach ( $result as $result_post ) {
			$post_terms = get_the_terms( $result_post, $taxonomy );
			$terms_array = wp_list_pluck( $post_terms, 'term_id' );
			if ( ! in_array( $excluded_term, $terms_array ) ) {
				$found_post = $result_post->ID;
				break;
			}
		}
	} else {
		$found_post = $result;
	}

	//If the post isn't found lets cache a value we'll check against. Add some variation in the caching so if a site is being crawled all the caches don't get created all the time.
	if ( empty( $found_post ) ){
		wp_cache_set( $query_key, "not found", 'default', 15 * MINUTE_IN_SECONDS + rand( 0, 15 * MINUTE_IN_SECONDS ) );
		return false;
	}

	wp_cache_set( $query_key, $found_post, 'default', 6 * HOUR_IN_SECONDS + rand( 0, 2 * HOUR_IN_SECONDS ) );
	$found_post = get_post( $found_post );

	return $found_post;
}

function wpcom_vip_attachment_url_to_postid( $url ){

	$id = wp_cache_get( "wpcom_vip_attachment_url_post_id_". md5( $url ) );
	if ( false === $id ){
		$id = attachment_url_to_postid( $url );
		if ( empty( $id ) ){
			wp_cache_set( "wpcom_vip_attachment_url_post_id_". md5( $url ) , 'not_found', 'default', 12 * HOUR_IN_SECONDS + mt_rand(0, 4 * HOUR_IN_SECONDS ) );
		}else {
			wp_cache_set( "wpcom_vip_attachment_url_post_id_". md5( $url ) , $id, 'default', 24 * HOUR_IN_SECONDS + mt_rand(0, 12 * HOUR_IN_SECONDS ) );
		}
	} else if( 'not_found' === $id ){
		return false;
	}
	return $id;
}

/**
 * Use this function to cache the comment counting in the wp menu that can be slow on sites with lots of comments
 * use like this:
 *
 * @param $post_id
 *
 * @see wp_count_comments()
 * @return bool|false|mixed|string
 */
function wpcom_vip_cache_full_comment_counts( $counts = false , $post_id = 0 ){
	//We are only caching the global comment counts for now since those are often in the millions while the per page one is usually more reasonable.
	if ( $post_id !== 0 ){
		return $counts;
	}
	$cache_key = "vip-comments-{$post_id}";
	$stats_object = wp_cache_get( $cache_key );

	//retrieve comments in the same way wp_count_comments() does
	if ( false === $stats_object ){
		$stats = get_comment_count( $post_id );
		$stats['moderated'] = $stats['awaiting_moderation'];
		unset( $stats['awaiting_moderation'] );
		$stats_object = (object) $stats;

		wp_cache_set( $cache_key, $stats_object, 'default', 30 * MINUTE_IN_SECONDS );
	}

	return $stats_object;

}

function wpcom_vip_enable_cache_full_comment_counts(){
	add_filter( 'wp_count_comments', 'wpcom_vip_cache_full_comment_counts', 10, 2 );
}


function wpcom_vip_enable_old_slug_redirect_caching(){
	add_action('template_redirect', 'wpcom_vip_wp_old_slug_redirect', 8 );
	// Hook the following actions to after the core's wp_check_for_changed_slugs - it's being hooke at prio 12
	add_action( 'post_updated', 'wpcom_vip_flush_wp_old_slug_redirect_cache', 13, 3 );
	add_action( 'attachment_updated', 'wpcom_vip_flush_wp_old_slug_redirect_cache', 13, 3 );
}

/**
 * This works by first looking in the cache to see if there is a value saved based on the name query var.
 * If one is found, redirect immediately. If nothing is found, including that there is no already cache "not_found" value we then add a hook to old_slug_redirect_url so that when the 'rea' wp_old_slug_redirect is run it will store the value in the cache @see wpcom_vip_set_old_slug_redirect_cache(). If we found a not_found we remove the template_redirect so the slow query is not run.
 */
function wpcom_vip_wp_old_slug_redirect(){
	global $wp_query;
	if ( is_404() && '' !== $wp_query->query_vars['name'] ) {

		$redirect = wp_cache_get('old_slug'. $wp_query->query_vars['name']  );

		if ( false === $redirect ){
			// Run the caching callback as the very firts one in order to capture the value returned by WordPress from database. This allows devs from using `old_slug_redirect_url` filter w/o polluting the cache
			add_filter( 'old_slug_redirect_url', 'wpcom_vip_set_old_slug_redirect_cache', -9999, 1 );
			//If an old slug is not found the function returns early and does not apply the old_slug_redirect_url filter. so we will set the cache for not found and if it is found it will be overwritten later in wpcom_vip_set_old_slug_redirect_cache()
			wp_cache_set( 'old_slug'. $wp_query->query_vars['name'], 'not_found', 'default', 12 * HOUR_IN_SECONDS + mt_rand(0, 12 * HOUR_IN_SECONDS ) );
		} elseif ( 'not_found' === $redirect ){
			//wpcom_vip_set_old_slug_redirect_cache() will cache 'not_found' when a url is not found so we don't keep hammering the database
			remove_action( 'template_redirect', 'wp_old_slug_redirect' );
			return;
		} else {
			/** This filter is documented in wp-includes/query.php. */
			$redirect = apply_filters( 'old_slug_redirect_url', $redirect );
			wp_redirect( $redirect, 301 ); //this is kept to not safe_redirect to match the functionality of wp_old_slug_redirect
			exit;
		}
	}
}
function wpcom_vip_set_old_slug_redirect_cache( $link ){
	global $wp_query;
	if ( ! empty( $link ) ){
		wp_cache_set( 'old_slug'. $wp_query->query_vars['name'], $link, 'default', 7 * DAY_IN_SECONDS );
	}
	return $link;
}

function wpcom_vip_flush_wp_old_slug_redirect_cache( $post_id, $post, $post_before ) {
	// Don't bother if slug hasn't changed.
	if ( $post->post_name == $post_before->post_name ) {
		return;
 	}

 	// We're only concerned with published, non-hierarchical objects.
 	if ( ! ( 'publish' === $post->post_status || ( 'attachment' === get_post_type( $post ) && 'inherit' === $post->post_status ) ) || is_post_type_hierarchical( $post->post_type ) ) {
		return;
 	}

 	// Flush cache for all old slugs.
 	$old_slugs = (array) get_post_meta( $post_id, '_wp_old_slug' );

 	foreach ( $old_slugs as $old_slug ) {
	    wp_cache_delete( 'old_slug' . $old_slug, 'default' );
    }

 	// FLush cache for new post_name since it could had been among old slugs before this update.
 	wp_cache_delete( 'old_slug' . $post->post_name, 'default' );
 }

/**
 * We're seeing an increase of urls that match this pattern: http://example.com/http://othersite.com/random_text
 * These then cause really slow lookups inside of wp_old_slug_redirect, since wp_old_slug redirect does not match on full urls but rather former slugs it's safe to skip the lookup for these. (Most of the calls are from bad ad providers that generate random urls)
 */
function wpcom_vip_maybe_skip_old_slug_redirect(){

	//We look to see if a malformed url (represented by 'http:' ) is right after the starting / in DOCUMENT_URI hence position 1
	if ( is_404() && ( false !== strpos( $_SERVER['DOCUMENT_URI'], parse_url( get_option( 'siteurl' ), PHP_URL_HOST ) ) ||  1 === strpos( $_SERVER['DOCUMENT_URI'], 'http:') || 1 === strpos( $_SERVER['DOCUMENT_URI'], 'https:') || false !== strpos( $_SERVER['DOCUMENT_URI'], 'https:/' ) || false !== strpos( $_SERVER['DOCUMENT_URI'], 'http:/' ) ) ) {
		remove_action( 'template_redirect', 'wp_old_slug_redirect' );
		remove_action( 'template_redirect', 'wpcom_vip_wp_old_slug_redirect', 8 );
	}

}
function wpcom_vip_enable_maybe_skip_old_slug_redirect(){
	add_action('template_redirect', 'wpcom_vip_maybe_skip_old_slug_redirect', 7 ); //Run this before wpcom_vip_wp_old_slug_redirect so we can also remove our caching helper
}

/**
 * Disable filename search on media search queries to prevent killed queries
 *
 * 4.7 introduces filename searching into media search queries. This unfortunately
 * makes those queries even less performant than they already were.
 *
 * @see https://core.trac.wordpress.org/changeset/38625
 *
 * @param  array $query The query arguments.
 * @return array        Altered query arguments.
 */
function wpcom_vip_disable_query_attachment_filenames( $clauses ) {
		remove_filter( 'posts_clauses', '_filter_query_attachment_filenames' );
		return $clauses;
}
add_filter( 'posts_clauses', 'wpcom_vip_disable_query_attachment_filenames', -999, 1 );

We hope this post helped you know which functions to use when building super fast WordPress themes or Plugins. Also, feel free to reach out to one of our experts to talk about the best ways we can help you speed up your website. If you enjoyed reading this article or it was a helpful resource, then please share it or leave a comment below and let us know what you think.

20 WordPress Core Functions That Don’t Scale and How to Work Around Them
5 (100%) 2 votes