The Advanced Guide to Optimizing WordPress Performance
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.
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 insidefooter.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 likequery_posts()
, but it does not modify the main query. On the other hand,get_posts()
by default performs the query with thesuppress_filters
parameter set totrue
. 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.
Originally published on Toptal.