post_class() and get_post_class() – Performance Killers for WordPress and WooCommerce

The get_post_class() function is a WordPress function commonly used within post “rivers”. For example, if I had a list of posts, WooCommerce products, or any content type really, I might have some code like this:


<div >

Note: post_class() just calls get_post_class() and outputs it to the browser.

post_class() will output something like class="post has-post-thumbnail type-POST-TYPE status-POST_STATUS tag-TAG1 tag-TAG2 category-CATEGORY1 category-CATEGORY2 ...."

The classes added make it easy to style content that has a specific taxonomy term, has a thumbnail, a particular status, etc.

However, the queries needed to determine all this information are not cheap. Moreover, this function is probably called for every post you’re listing. So if you have posts_per_page set to 20, this function will be called 20 times.

Let’s take a look at the function’s code (I’ve trimmed some of the comments):

function get_post_class( $class = '', $post_id = null ) {
	$post = get_post( $post_id );
	$classes = array();
	if ( $class ) {
		if ( ! is_array( $class ) ) {
			$class = preg_split( '#s+#', $class );
		}
		$classes = array_map( 'esc_attr', $class );
	} else {
		// Ensure that we always coerce class to being an array.
		$class = array();
	}
	if ( ! $post ) {
		return $classes;
	}
	$classes[] = 'post-' . $post->ID;
	if ( ! is_admin() )
		$classes[] = $post->post_type;
	$classes[] = 'type-' . $post->post_type;
	$classes[] = 'status-' . $post->post_status;
	// Post Format
	if ( post_type_supports( $post->post_type, 'post-formats' ) ) {
		$post_format = get_post_format( $post->ID );
		if ( $post_format && !is_wp_error($post_format) )
			$classes[] = 'format-' . sanitize_html_class( $post_format );
		else
			$classes[] = 'format-standard';
	}
	$post_password_required = post_password_required( $post->ID );
	// Post requires password.
	if ( $post_password_required ) {
		$classes[] = 'post-password-required';
	} elseif ( ! empty( $post->post_password ) ) {
		$classes[] = 'post-password-protected';
	}
	// Post thumbnails.
	if ( current_theme_supports( 'post-thumbnails' ) && has_post_thumbnail( $post->ID ) && ! is_attachment( $post ) && ! $post_password_required ) {
		$classes[] = 'has-post-thumbnail';
	}
	// sticky for Sticky Posts
	if ( is_sticky( $post->ID ) ) {
		if ( is_home() && ! is_paged() ) {
			$classes[] = 'sticky';
		} elseif ( is_admin() ) {
			$classes[] = 'status-sticky';
		}
	}
	// hentry for hAtom compliance
	$classes[] = 'hentry';
	// All public taxonomies
	$taxonomies = get_taxonomies( array( 'public' => true ) );
	foreach ( (array) $taxonomies as $taxonomy ) {
		if ( is_object_in_taxonomy( $post->post_type, $taxonomy ) ) {
			foreach ( (array) get_the_terms( $post->ID, $taxonomy ) as $term ) {
				if ( empty( $term->slug ) ) {
					continue;
				}
				$term_class = sanitize_html_class( $term->slug, $term->term_id );
				if ( is_numeric( $term_class ) || ! trim( $term_class, '-' ) ) {
					$term_class = $term->term_id;
				}
				// 'post_tag' uses the 'tag' prefix for backward compatibility.
				if ( 'post_tag' == $taxonomy ) {
					$classes[] = 'tag-' . $term_class;
				} else {
					$classes[] = sanitize_html_class( $taxonomy . '-' . $term_class, $taxonomy . '-' . $term->term_id );
				}
			}
		}
	}
	$classes = array_map( 'esc_attr', $classes );
	$classes = apply_filters( 'post_class', $classes, $class, $post->ID );
	return array_unique( $classes );
}

Within this code, the following functions might result in database queries: get_post_format, has_post_thumbnail, is_sticky, and get_the_terms. The most expensive of these queries is get_the_terms which for each taxonomy associated with the post type, selects all the terms attached to the post for that taxonomy. If there are four taxonomies associated with the post type being queried, get_post_class could result in 7 extra database queries per post. With 20 posts per page, that’s an extra 140 queries per page load! On WooCommerce sites where there are many taxonomies and usually many products per page being shown, this is a huge performance killer. Yes, object caching (and page caching of course) will improve our eliminate some of the database queries, but people will still be hitting the cache cold sometimes.

Solution:

Don’t use get_post_class or post_class. It’s not that important. 99% of people don’t use the tags it generates. What I do is output the function, inspect the classes it adds using Chrome, and hardcode the classes actually referenced in CSS into the theme.

PS: body_class() is much less query intensive and okay to use.


Comments

4 responses to “post_class() and get_post_class() – Performance Killers for WordPress and WooCommerce”

  1. Maybe, need to add a “short-circuit” filter at the beginning of `get_post_class`. Even before the `get_post`.

    1. Taylor Lovett Avatar
      Taylor Lovett

      Definitely a good idea.

  2. Wacław Jacek Avatar
    Wacław Jacek

    Cool! Do you have any specific data on how much faster a theme works without the extra queries?

    1. Taylor Lovett Avatar
      Taylor Lovett

      Nope, too many variables to really measure that.

Leave a Reply

Your email address will not be published. Required fields are marked *