Today, WordPress powers 25% of the Internet. It’s easy to use, incredibly popular, and not going anywhere anytime soon.

But WordPress can be slow. So how do you optimize it?

There are loads of articles about how to tune and optimize WordPress. In fact, WordPress itself provides a robust guide on WordPress optimization.

For the most part, these articles and tutorials cover pretty basic yet useful concepts, like using cache plugins, integrating with content delivery networks (CDNs), and minimizing requests. While these tips are highly effective and even necessary, in the end, they don’t address the underlying problem: Most slow WordPress sites are a result of bad or inefficient code.

The Advanced Guide to Optimizing WordPress Performance

WordPress can be slow, but it doesn't have to be.

Therefore, this article is mainly aimed at providing developers with some guidelines that can help them address the underlying causes of many WordPress performance issues.

WordPress provides many performance-oriented features that are often overlooked by developers. Code that doesn’t leverage these features can slow down the simplest of tasks , such as fetching posts. This article details four possible solutions, which address some of the underlying problems behind slow WordPress performance.

Fetching Posts

WordPress offers the possibility of fetching any kind of post from the database. There are three basic ways of doing so:

  • Using the query_posts() function: This is a very direct approach, but the problem is that it overrides the main query, which could lead to inconveniences. For example, this could be an issue if we wanted to determine, at some point after fetching the posts (such as inside footer.php), what kind of page we are dealing with. In fact, the official documentation has a note recommending against the use of this function as you will need to call an additional function to restore the original query. Moreover, replacing the main query will negatively impact page loading times.

  • Using the get_posts() function: This works almost like query_posts(), but it does not modify the main query. On the other hand, get_posts() by default performs the query with the suppress_filters parameter set to true. This could lead to inconsistencies, especially if we use query-related filters in our code, as posts that you are not expecting in a page may be returned by this function.

  • Using the WP_Query class: In my opinion, this is the best way to retrieve posts from the database. It does not alter the main query, and it is executed in its standard way, just like any other WordPress query.

But whichever method we use to interact with the database, there are other things we need to consider.

Limiting the Query

We should always specify how many posts our query must fetch.

In order to accomplish that, we use the posts_per_page parameter.

WordPress lets us indicate -1 as a possible value for that parameter, in which case the system will try to fetch all the posts that meet the defined conditions.

This is not a good practice, even if we are certain that we’ll only get a few results back as the response.

For one, we rarely can be certain about only getting a few results back. And even if we can, setting no limit will require the database engine to scan the entire database looking for matches.

Conversely, limiting the results often enables the database engine to only partially scan the data, which translates into less processing time and faster response.

Another thing that WordPress does by default, which can adversely impact performance, is that it tries to bring sticky posts and calculate how many rows were found on the query.

Often, though, we don’t really need that information. Adding these two parameters will disable those features and speed up our query:

$query = new WP_Query( array(
	'ignore_sticky_posts'	=> true,
	'no_found_rows'		=> true
	)
); 

Excluding Posts from the Query

Sometimes we want to exclude certain posts from the query. WordPress offers a pretty direct way of achieving it: using the post__not_in parameter. For example:

$posts_to_exclude 	= array( 1, 2, 3 );
$posts_per_page	= 10;


$query = new WP_Query( array(
	'posts_per_page'	=> $posts_per_page,
	'post__not_in'		=> $posts_to_exclude
	)
);


for ( $i = 0; $i < count( $query->posts ); $i++ ) {
	//do stuff with $query->posts[ $i ]
}

But while this is pretty simple, it’s not optimal because internally it generates a subquery. Especially in large installations, this can lead to slow responses. It’s faster to let that processing be done by the PHP interpreter with some simple modifications:

$posts_to_exclude 	= array( 1, 2, 3 );
$posts_per_page	= 10;


$query = new WP_Query( array(
	'posts_per_page'	=> $posts_per_page + count( $posts_to_exclude )
	)
);


for ( $i = 0; $i < count( $query->posts ) && $i < $posts_per_page; $i++ ) {
	if ( ! in_array( $query->posts[ $i ]->ID, $posts_to_exclude ) ) {
		//do stuff with $query->posts[ $i ]
	}
}

What did I do there?

Basically, I took off some work from the database engine and left it instead to the PHP engine, which does the same stuff but in memory, which is way faster.

How?

First, I removed the post__not_in parameter from the query.

Since the query may bring us some posts that we do not want as a result, I increased the posts_per_page parameter. That way I ensure that, even if I had had some undesired posts in my response, I would have at least $posts_per_page desired posts there.

Then, when I loop over the posts I only process those which are not inside the $posts_to_exclude array.

Avoiding Complex Parameterization

All these query methods offer a wide variety of possibilities for fetching posts: by categories, by meta keys or values, by date, by author, etc.

And while that flexibility is a powerful feature, it should be used with caution because that parametrization could translate into complex table joins and expensive database operations.

In the next section, we will outline an elegant way to still achieve similar functionality without compromising performance.

Squeezing the Most out of WordPress Options

The WordPress Options API provides a series of tools to easily load or save data. It’s useful for handling small pieces of information, for which other mechanisms that WordPress offers (like posts or taxonomies) are overly complex.

For example, if we want to store an authentication key or the background color of our site’s header, options are what we are looking for.

WordPress not only gives us the functions to handle them, but it also enables us to do so in the most efficient way.

Some of the options are even loaded directly when the system starts, thus providing us with faster access (when creating a new option, we need to consider whether we want to autoload it or not).

Consider, for example, a site on which we have a carousel displaying breaking news specified in the back-end. Our first instinct would be to use a meta key for that as follows:

// functions.php
add_action( 'save_post', function ( $post_id ) {
	// For simplicity, we do not include all the required validation before saving
	// the meta key: checking nonces, checking post type and status, checking
	// it is not a revision or an autosaving, etc.
	update_post_meta( $post_id, 'is_breaking_news', ! empty ( $_POST['is_breaking_news'] ) );
} );


// front-page.php
$query = new WP_Query( array(
	'posts_per_page'	=> 1,
	'meta_key'		=> 'is_breaking_news'
	)
);
$breaking_news = $query->posts[0] ?: NULL;

As you can see, this approach is very simple, but it is not optimal. It will perform a database query trying to find a post with a specific meta key. We could use an option to achieve a similar result:

// functions.php
add_action( 'save_post', function ( $post_id ) {
	// Same comment for post validation
	if ( ! empty ( $_POST['is_breaking_news'] ) )
		update_option( 'breaking_news_id', $post_id );
} );


// front-page.php
if ( $breaking_news_id = get_option( 'breaking_news_id' ) )
	$breaking_news = get_post( $breaking_news_id );
else
	$breaking_news = NULL;

The functionality slightly varies from one example to another.

In the first piece of code, we will always get the latest breaking news, in terms of the post’s published date.

In the second one, every time a new post is set as breaking news, it will overwrite the previous breaking news.

But because we probably want one breaking news post at a time, it should not be a problem.

And, in the end, we changed a heavy database query (using WP_Query with meta keys) into a simple and direct query (calling get_post()) which is a better and more performant approach.

We could also make a small change, and use transients instead of options.

Transients work similarly but allow us to specify an expiration time.

For example, for breaking news, it fits like a glove because we don’t want an old post as breaking news, and if we leave the task of changing or eliminating that breaking news to the administrator, [s]he could forget to do it. So, with two simple changes, we add an expiration date:

// functions.php
add_action( 'save_post', function ( $post_id ) {
	// Same comment for post validation
	
	// Let's say we want that breaking news for one hour
	// (3600 =  # of seconds in an hour).
	if ( ! empty ( $_POST['is_breaking_news'] ) )
		set_transient( 'breaking_news_id', $post_id, 3600 ); 
} );


// front-page.php
if ( $breaking_news_id = get_transient( 'breaking_news_id' ) )
	$breaking_news = get_post( $breaking_news_id );
else
	$breaking_news = NULL;

Enable Persistent Caching

WordPress natively has an object caching mechanism.

Options, for example, are cached using that mechanism.

But, by default, that caching is not persistent, meaning that it only lives for the duration of a single request. All data is cached in memory, for faster access, but it is only available during that request.

Supporting persistent caching requires the installation of a persistent cache plugin.

Some full-page cache plugins come with a persistent cache plugin included (for example W3 Total Cache), but others do not, and we need to install it separately.

It will depend on the architecture of our platform, whether we will use files, Memcached or some other mechanism to store cached data, but we should take advantage of this amazing feature.

One might ask: “If this is such a great feature, why doesn’t WordPress enable it by default”?

The main reason is that, depending on the architecture of our platform, some cache techniques will work and others will not.

If we host our site in our distributed server, for example, we should use an external cache system, (such as a Memcached server), but if our website resides on a single server, we could save some money by simply using the file system to cache.

One thing that we need to take into account is cache expiration. This is the most common pitfall of working with persistent caching.

If we don’t address this issue correctly, our users will complain that they will not see the changes they have made or that their changes took too long to apply.

Sometimes we are going to find ourselves making tradeoffs between performance and dynamism, but even with those obstacles, persistent caching is something that virtually every WordPress installation should take advantage of.

AJAXing the Fastest Way

If we need to communicate via AJAX with our website, WordPress offers some abstraction at the time of processing the request on the server side.

Even though those techniques can be used when programming back-end tools or form submissions from the front-end, they should be avoided if it is not strictly necessary.

The reason for this is that in order to use those mechanisms, we are obligated to make a post request to some file located inside the wp-admin folder. The majority (if not all) of WordPress full-page caching plugins neither cache post requests nor calls to administrator files.

For example, if we dynamically load more posts when the user is scrolling our homepage, it would be better to directly call to some other front-end page, which will get the benefits of being cached.

We could then parse the results via JavaScript in the browser.

Yes, we are sending more data than we need to, but we are winning in terms of processing speed and response time.

Destroy the Notion That WordPress is Just Slow

These are just a few pieces of advice that developers should consider when coding for WordPress.

Sometimes, we forget that our plugin or theme might need to live together with other plugins, or that our site may be served by a hosting company that serves hundreds or thousands of other sites with a common database.

We just focus on how the plugin should function and not on how it deals with that functionality, or how to do it in an efficient way.

From the above, it is clear that root causes of poor performance in WordPress are bad and inefficient code. However, WordPress provides all the necessary functionalities through its various APIs that can help us build much more performant plugins and themes without compromising the speed of the overall platform.

About the author

Martín Di Felice, Argentina
member since January 17, 2016
Martin considers himself a full-stack developer who's currently working on back-end development projects. For the last five years, Martín has been working on WordPress sites including themes and plugins hosted at the WordPress VIP platform. He's looking for all sorts of projects as long as the work provides a challenge. [click to continue...]
Hiring? Meet the Top 10 Freelance WordPress Developers for Hire in March 2017

Comments

Dr. Polar Humenn
It is generally considered that having the database do your queries should be faster due to internal optimized implementation at the database level and also the limiting the data transfer time. Have you any data supporting your claims that Wordpress queries are actually slower in this fashion? What is the break even point? I agree that putting a limit on the number of objects to return from a query will limit an exhaustive search from the database, and would be quicker, if there were actually very many posts. However, I am suspect about your approach bring the search "into memory" and that would be quicker. What if your array of excluded posts is large? I am assuming "in_array" does a straight linear search, which is O(N), unless you performed a sorted version of the excluded array and used something else. Then again, to transfer the data just to throw it away, may lead to some performance hits as well.
dimasmagadan
I would argue with some items. >Fetching Posts >Limiting the Query >Excluding Posts from the Query in some situations things advised there are a bit useless. If we are using any caching plugin and the page is fetched from cache, not generated every time, the effect of such an optimization will be much smaller. >Squeezing the Most out of WordPress Options All the options, that are saved with 'autoload'='yes' are loaded for each WordPress instance. So if we store the ID of those 'breaking' post in options, it will be loaded for every page, not only for that page where we really need that ID. Loading one extra number will not eat much memory. But if we store all other things in a same way, there could be a noticeable impact on memory consumption. >AJAXing the Fastest Way Calling a front-end page will be not efficient. Because such a page will load all the WordPress engine to fetch that page. To my mind it is always better to use ajax as it is described in documentation. But in some rare cases it could make sense to create a custom end-point or a separate php script, that will load WordPress with define('SHORTINIT', true);
Martín Di Felice
Hi Polar. Of course it is convenient to let the database engine to perform data related processing, at least in most cases. But I refer to some specific cases, for example when you use the post__not_in parameter, that translates in not so optimal database operations like subqueries or full table reads. And also it is important to know which amount of data are you dealing with. I'm talking about no more than a hundred posts. Regards!
Martín Di Felice
Hi! About making queries, you are right. You should be using a cache plugin, and that optimization will not be used most of the time. But it's true that almost all our sites run in a shared environment. Maybe there a lot of other sites running there and we need to make our code the more efficient we can to avoid an impact on the whole platform. About the options, you are absolutely right. You should choose wisely which options should be autoloaded and which should not. And finally, about the AJAX, you are wrong (at least, partially :)). If you use a cache plugin (and you should), most of them cache front end requests, and those cached request will be returned most of the time. Of course, you will lose on data transfer, but you will win a lot in processing, memory, etc. which generally are more expensive resources. Using a custom endpoint could be the perfect solution, we could take advantage of the full page caching and also optimize what we transfer. Thanks for the comment!
Dr. Polar Humenn
Agreeing with me partially is not a good way to dismiss this comment. Do you have any real hard timing data supporting your claims? Or perhaps, your claim just a conjecture based on some Wordpress internal knowledge? What conditions did you encounter that lead you to implement searches in such a fashion? I think the community would benefit from this information, than merely just your claims and recommendations. Also, I would like to know why a post_not_in parameter would cause a "full table read", otherwise known as a "full table scan". Or, why it would cause a subquery? And does Wordpress even use SQL sub-queries under the covers? What would be the example of a query you would use in Wordpress that would actually cause a full table scan or a subquery merely by using a post_not_in parameter?
Martín Di Felice
Hi again Polar! If you want to know more why not to use post__not_in check this out: https://vip.wordpress.com/documentation/performance-improvements-by-removing-usage-of-post__not_in/ It was written by the Automattic people. Regards!
Dr. Polar Humenn
Thanks Martin. Unfortunately, the article to which you refer doesn't say anything about full table scans or sub-queries. It only posits that using post_not_in *may* affect the query cache hit rate, but doesn't state the reasons for that condition. That condition would be dependent upon the implementation handling the post_not_in parameter, and the implementation is not discussed. Ultimately, there appears to be no data supporting this claim, and it's merely conjecture at this point. Thanks for your time.
Martín Di Felice
No problem. I was rather vague on my first response, since I was referring to the whole article. For example, when you use meta queries on some meta value, since this field is not indexed, it may produce a full table scan. Check https://codex.wordpress.org/Class_Reference/WP_Query#Custom_Field_Parameters and https://codex.wordpress.org/Database_Description#Indexes_5 About the post__not_in issue, to be more specific, it allows to extract some posts from the search. And it is possible that those posts can vary from one page to another, or from one user to another. If we perform one unique query, the database and WP itself, may cache that same query for all the concurrent users. And we can extract those conflicting posts via PHP, which is faster since it does not involve communication with the database or IO operations. The goal of this article is to give a guide or a serie of tips. It is good to establish a discussion to know why I am making those suggestions and I tried to give a brief explanation for them, but in order to maximize the amount of tips or suggestions, I need to summarize the contents of my article. Otherwise I would finish writing a book that possibly nobody would read (even if I had the time to do it). I think it is positive to continue with a discussion as long it is constructive. I don't think this is the case, so sorry if I don't answer any more. Best of luck!
Dr. Polar Humenn
No apologies necessary.
dimasmagadan
Not sure where is my previous comment. Possibly was removed because of links to some other sites or I didn't press send that time :) To make it short: the way you suggest to use ajax (a WordPress page as an endpoint) is totally wrong. If you need a less memory consuming solution a standalone php script that will load only needed WordPress parts could help. Another way to gain some performance is to use REST API. According to some tests it is faster.
Martín Di Felice
It always depends what you are doing in your AJAX call. Using a standalone PHP file could lead you to bypass the cache system and a lot of hooks and you are not sure what is going to happen with future updates. There are a lot of drawbacks, and I would not recommend it as a good practice. Using the REST API is another thing. Since it is kind of new I did not include it in the article but it is indeed a good choice.
GRSoft Solutions
I am thankful of you that you shared a great blog and i read your articles regularly, it help me alot for my websites to make it best websites. http://www.grsoftsolution.com/web-applications-development-company-india.php
GRSoft Solutions
Website development company india http://www.grsoftsolution.com/web-applications-development-company-india.php
Leon Cooper
https://bricksthestore.com/ running on shared hosting still pulled 98 on page speed insights but my theme has been changed and functionality added since then so I admit that using theses practices aside many others are effective #seo
Amir
When you query posts wordpress by default execute query where select all published posts. If you add "post__not_in" parameter to query args wordpress perform additional query identical as default one except it adds NOT IN
comments powered by Disqus
Subscribe
The #1 Blog for Engineers
Get the latest content first.
No spam. Just great engineering posts.
The #1 Blog for Engineers
Get the latest content first.
Thank you for subscribing!
You can edit your subscription preferences here.
Trending articles
Relevant Technologies
About the author
Martín Di Felice
JavaScript Developer
Martin considers himself a full-stack developer who's currently working on back-end development projects. For the last five years, Martín has been working on WordPress sites including themes and plugins hosted at the WordPress VIP platform. He's looking for all sorts of projects as long as the work provides a challenge.