How to improve WordPress performance making simple architectural decisions?

Response times increase along with the project size and the level of data complexity and at some point every query matters. This article describes how to achieve better performance results, just by making simple architectural decisions before project start.

Milliseconds Make Millions. This intriguing statement may seem strange, but it fully reflects the essence of crafting solutions that prioritize optimization, which based on my observations is unique rather than the default aspect in many WordPress projects.

Clients often ask us for help in this area, sometimes just even after a lot of money on the project. In many cases, it turns out that the developer did not care about anything other than delivering the project on time. Finally, they installed Autoptimize, a caching plugin and tell that this was enough. Classical painting grass green.

Tip: Don't tell me that you haven't experienced this as author nor as analyst 😏

I was doing the same until I realised that the success of performance results relies on the , even before writing a line of code. So in this article, I'll show you a few small-effort tricks that should help you create optimization-oriented solutions from the beginning. I'll try to prove that making simple architectural decisions might boost up your app performance from the beginning which can contribute to your success.

Tip: Before I start I insist you to check out a about measuring execution times. It might be useful during the optimization process!

Reach me out on Twitter know what do you think!


What are the testing conditions?

I'll work on the database with 2M entries and not every WordPress website operates with such numbers. The results for simpler systems may be smaller, but still noticeable.

According to the report created by Deloitte team, even the smallest improvement might have a positive effect on business results for brand. The findings showed that a mere 0.1s reduction in load times can lead to a remarkable 10% increase in conversion rates.

Results #0

Chart above present a results of optimization process in one of the project I've made some time ago, which size was much smaller thant 2M posts. The tips from this article for sure are not the only ones that contributed to this success, but are a really important part of it. Remember that even the smallest boost can contribute to your success 😈

To have something to work on, I'll start from measuring simple thing like the time spent on querying the latest 1000 posts. It's nothing fancy and demanding but allows to verify results and confirm if I'm going in the right direction.

get_posts([
  'post_type' => 'post',
  'posts_per_page' => 1000,
]);

Time: 2.0892357826233s

The standard query takes about two seconds which isn't really much, especially on local environment that is mostly slower that production. However, please note that it's only one query on the page. More queries, components, or plugins makes it crucial.


#1 Do I need to use pagination?

By default, when performing a WP_Query, SQL_CALC_FOUND_ROWS in the SQL to determine the total number of rows matching the criteria. This is done to handle a classic pagination component that in most cases displays the latest available page in the archive. In some cases, especially for larger websites it might be demanding.

Let's say that I need to create a widget that displays the latest 1000 posts. Do I need a pagination in this specific case? Totally not. Such calculation is not needed and I can disable this by setting no_found_rows parameter to true. What are the results then?

$posts = get_posts([
  'post_type' => 'post',
  'posts_per_page' => 1000,
  'no_found_rows' => true,
]);

Time: 1.6744859218597s

One simple argument reduced the querying time by 20%. Some people might say "That's only 20%", but I think that even if we gain ~5% improvements by this change, it is worth it, especially if it's just adding one simple parameter and doing exactly what I need.

Let's say now, that I know that the system I build will have a lot of content and users from beginning. Knowing that I'm able to reduce query times by 20% I can display next page button instead of a classic pagination. At some point, users will catch no results, but I don't consider it as a problem if we show good looking 404 page. Making simple architectural decision helps reducing querying times by 20% by default 🔥

Question: What about you guys? Do you have any other cases where this process might be useful?

I've noticed that the best results are achieved with large databases or heavy websites. So is it really worth remembering? Totally. I can never be sure in what circumstances my app will be working, so if I can reduce potential problems by default, especially in such simple cases, I'll take it. That's how I try to build apps fast by default.


#2 What do I exactly want?

That's a simple, but really important question. WP_Query by default returns a collection of WP_Post objects that match the criteria. In some cases the only thing I need is to get a collection of post IDs, so building full objects to map them again to ID's .I can inform WordPress that I need just IDs by setting fields parameter to ids.

$posts = get_posts([
  'post_type' => 'post',
  'posts_per_page' => 1000,
  'fields' => 'ids',
]);

Time: 0.017959833145142s

Defining what I exactly want improved querying times 122x 🔥 Since those are results I wish to always get, I must admit that it's not as perfect as it looks. It's a really useful trick in the huge system I build and has a positive impact on performance, but in most cases, the collection of IDs is not enough. Let's expand a query and get post meta too.

$posts = get_posts([
  'post_type' => 'post',
  'posts_per_page' => 1000,
  'fields' => 'ids',
  'no_found_rows' => true,
]);

$posts = array_map(fn($id) => [
  'id' => $id,
  'meta' => get_post_meta($id, 'views', true),
], $posts);

Time: 0.67373204231262s

Asking WordPress only for the things I need in this case improved querying times 3x. That's much less than earlier, but still A LOT. This simple trick saves ~140 seconds per every 100 requests compared to the standard query, and 100 requests is not a lot...

Question: Do you have any usecases for querying just ID's in mind? In the project I made we have a few ones, but they are really custom. That's why I'm curious.

It is important to add that it applies mostly to simple queries that don't need to check other tables. For instance, if we add meta_query or tax_query, response times are much worse, because SQL still needs to to narrow down the results.


#3 Do I need to filter results?

Planning the data architecture isn't a common habit in many WordPress projects. When custom data needs to be stored for posts, it mostly lands in the postmeta table.

Question: Tell me honestly - how often do you think about this? If you think about this regulary, when you started realising that?

It is fine for the data that needs to be displayed, but not for the one that should be used for filtering and narrowing search results. So before I define custom data for the posts I ensure if it should/can be used for filtering. If so, .

$posts = get_posts([
    'post_type' => 'post',
    'posts_per_page' => 1000,
    'meta_query' => [
        'realation' => 'AND',
        [
            'key' => 'sport',
            'value' => 'Football',
        ],
    ],
]);

Time: 9.48s

In the example above ⬆️ the sport data is stored as a string value in the postmeta table. Result below ⬇️ uses sport as a term in sport taxonomy. The difference is significant. Using taxonomies results in 3x faster queries than when using post meta. You may be wondering How is it possible? and relies in the database design.

$posts = get_posts([
    'post_type' => 'post',
    'posts_per_page' => 1000,
    'tax_query' => [
        'realation' => 'AND',
        [
            'taxonomy' => 'sport',
            'field' => 'slug',
            'terms' => 'football',
        ],
    ],
]);

Time: 3.23s

Unfortunately, It shouldn't be applied to all the data types. It works well for attribute like sport because the set is finite. Using taxonomies for filtering by athlete which is an infinity set may bring some other problems and is not recommended. So it needs to be . Also, for really huge systems some are needed.


Summary

The performance difference between those options may not always be significant, and it can vary depending on the specific use case and the optimizations in place. So I can't say something like "always use them". I would rather ask you to give them try and check what impact those might have for your project!

SizeOriginalPaginationIds
10000.23s0.23s0.006s
100000.22s0.23s0.006s

For example, the table above presents the results with clean WordPress with 1000 and 10000 simple posts. There is no difference between default query and the one that skips pagination, but improvement caused by returning ID's still is noticeable. You might think that those numbers can be not comparable with your projects, so...

Test it on your own! Maybe you're struggling with performance issues, you think that you've done everything what's possible and the results are still bad? Try to use the snippet below to test the results in your system and feel the real impact on your own!

add_action('template_redirect', function () {
  $start = microtime(true);
  $posts = get_posts([
    'post_type' => 'post',
    'posts_per_page' => 1000,
    'fields' => 'ids',
    'no_found_rows' => true,
  ]);
  echo microtime(true) - $start;
});

Feedback

How satisfied you are after reading this article?