Benchmarking WordPress for Better Results and Less Pain

picture of a stopwatch

With such a robust ecosystem, bursting at the seams with 3rd-party solutions, there’s no wonder why WordPress has held the CMS crown for so long. Plugins, themes, and services are plentiful, but along with the endless combinations, comes drastic variations in performance. Variations that, without benchmarks, go unnoticed.

Every variable has some level of impact on the site’s overall performance – something that’s often taken for granted until it snowballs out of control. A few milliseconds here and there don’t seem like a lot, but over time, they can add up.

In this article, we’re going to show you how to perform benchmark tests on your WordPress site.
By performing these benchmarks regularly, you’ll be able to keep track of your site’s performance over time, track performance woes, and keep your site running in peak health.

Benchmarking Basics For the Uninitiated

We realize that benchmarking is a deep topic, full of nuance and different perspectives, approaches, and tools. Even if you’ve heard about many of the concepts before, you still might not understand how everything fits together.

That’s okay.

If you get a little lost, take a look at our recent article about how to understand WordPress benchmarking results. There’s a good chance that it’ll get you back on track.

Worst case, if you hit a snag, just keep reading. There are several concepts to discuss, and even if one isn’t quite your speed, another might be.

Anyways, with that out of the way, let’s get started.

Performing Basic Raw Performance Tests

The most basic of tests is to check raw performance. This is done by simply running a piece of code, often something CPU intensive like encryption, then seeing how long it takes to complete.

Since there’s no sense in reinventing the wheel, we’ll refer you to the WPPerformanceTester plugin by ReviewSignal. (Yes, we know it hasn’t been updated in a while. It still works fine though.)

Although its tests are isolated to the raw performance of your hosting environment, it’s a good starting point to determine some baseline data.

Performing Load Tests

Load testing is a completely different beast. As we discussed in our article about understanding WordPress benchmark results, load testing is all about seeing how your site performs under stress.

The most popular tool for performing load testing is k6. It’s reliable, open source, and can be as basic or complex as the tests you write for it.

For the purposes of this article, we’ll skip the basic installation and usage process, but if you need it, you can find a k6 quickstart guide within their documentation.

Test Methodology

Before we start tossing out configs, let’s talk a little about the methodology behind running these tests. Specifically, how and why you’ll want to run certain tests on different WordPress environments.

In a perfect world, we’d test everything, in every possible way. In a perfect world, we’d all poop pumpkin spice frosting too.

Your tests can and should become more robust over time, but what’s most important is to test the parts that are most likely to fail. For WordPress sites, this means pages that can’t be cached, particularly ones that perform a lot of database operations or other heavier tasks.

For example, a few good places to start testing are:

  • Any ecommerce pages that display a user’s cart
  • Checkout pages
  • User login and account pages
  • Large archives, especially if there’s any sort of filtering available
  • Subcategories, tags, recommended posts, or anything else that is likely to have lots of variation

If you’re not sure, spam it with some users and see what happens while caching if on, then test again with caching off. If the page isn’t significantly faster when caching is on, you’ve found a good candidate to include in your load tests.

Example Configuration

import http from 'k6/http';
import { sleep, check } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 20 },
    { duration: '1m30s', target: 10 },
    { duration: '20s', target: 0 },
  ],
};

export default function () {
  const res1 = http.get('https://example.com/');
  check(res1, { 'status was 200': (r) => r.status == 200 });
  sleep(Math.random() * 30);
  
  const res2 = http.get('https://example.com/product/wordpress-pennant/');
  check(res2, { 'status was 200': (r) => r.status == 200 });
  sleep(Math.random() * 30);

  const res3 = http.get('https://example.com/page/2/');
  check(res3, { 'status was 200': (r) => r.status == 200 });
  sleep(Math.random() * 30);

  const res4 = http.get('https://https://example.com/2023/07/18/post-with-a-form-and-stuff/');
  check(res4, { 'status was 200': (r) => r.status == 200 });
  sleep(Math.random() * 30);
}

Combining and Enhancing Your Benchmarks

Once you’ve written a few tests, you’re going to find areas that you want to improve upon. If a page performs poorly and doesn’t scale well, you’ll want to optimize it. The reason is simple: it’s often better to optimize code than to scale infrastructure.

Even if you don’t already have an in-house development team, paying an agency or freelancer to optimize it for you is often cheaper in the long run. If your site slows down or goes offline from a traffic spike, you lost potential customers. If your host is nickel and diming you with their “moar PHP w3rkers plz” excuse, it’s going to continue to scratch away at your profits for all of eternity.

Get Granular

GIF from Fear and Loathing in Las Vegas that says "we can't stop here. this is bat country"

Benchmarks don’t have to be restricted to a whole page render – you can benchmark specific segments of your site’s code, down to the nanosecond. Remember that a WordPress site is made up of several different moving parts.

Start testing the parts to find the rusty old bolt.

Buckle in, we’re diving deep into code territory. If you’re not a dev and don’t understand any of this next section, keep reading, but consider hiring someone who would understand it. Do more of what you’re good at, hire someone who loves the things you don’t.

Like hosting. Want some hosting? We’ll sell you some.

You could use services like New Relic, but that’s more effort to get up and running, likely a subscription, and then you still have to learn how to use it. Alternatively, you could toss XDebug into profile mode and test locally, but the hosting environments aren’t going to match up.

Each solution has its benefits and drawbacks, but there’s another solution that you might not have considered: WordPress is built to be extensible… Just extend it to be more verbose.

Using Hooks as a Benchmarking Signal

There are hooks all over WordPress core. We can use them as mile markers and signposts. By looking at the hooks that are firing in any given request, you can see what it’s doing, why, when, and for how long. Plus, you can do it on your production environment if you really need to.

(For real though, don’t do this on a production environment unless you have no other choice. Cowboy coders deserve to be shamed – cowboy debuggers deserve to be mercilessly ridiculed. Myself included.)

To see what I mean, take a look at this quick little snippet:

<?php

add_action('shutdown', function() {
    global $wp_actions;
    var_dump($wp_actions);
});

“But that’s just a list of actions! I can get a list of actions from Query Monitor!” Yeah, I know. Keep your pants on – we’re getting there.

Now that we know all of the actions that are firing on the page, we can start timing things.

If you want to get the time of an action, just hook into the action, grab the time, and toss it somewhere like a global like this:

function add_to_time_global($hook) {
    global $time = array();

    $time[] = array( current_filter() => hrtime(true) );
}

function output_stuff() {
    global $time;
    var_dump( $time );
}

add_action('loop_start', 'add_to_time_global');
add_action('loop_end', 'add_to_time_global');
add_action('shutdown', 'output_stuff')

This tells us how long things take, at least in a very basic sense. We know the exact nanosecond that each hook fired, and how many times. We can time every hook in WordPress if we want.

Although this alone can do the trick for debugging, it still doesn’t help very much with benchmarking. It’s hardly practical or a real picture of what would happen under stress. It’s not like we can use it during load tests.

Or can we..?

Mocking Functionality

If we know what we’re testing, we can mock up a version of the code that takes measurements throughout the process. We could either log it, or even better – return it as a REST API response.

By returning it as a REST API response, we can integrate it into our load tests for automated testing. It also means that we can perform small tests from the WordPress admin, using client-side requests.

A server-side script can run something a bunch of times, but will only tell you how a single worker thread is performing. If you send multiple client-side requests instead, similar to a mini version of a load test, you get a much more accurate picture without any other tooling.

Overengineering a Stopwatch

For those of you who know the author of this post: did you really think I could write something like this without overengineering some sort of monstrosity?

While writing this article, I did a lot of thinking about how someone could start running benchmarks with as little friction as possible. I wanted something practical, portable, and with a low barrier of entry. Even users without much knowledge of the underlying details should be able to compare numbers with the click of a button.

Furthermore, the plugin should be extensible – encouraging others to write their own tests for specific scenarios, 3rd party plugins, or to contribute more robust tests that anyone can run easily.

Thus, WP Benchy was born. User clicks button, button runs tests, user gets numbers.

Let’s Write a Test

Without further ado, let’s write up a test that uses WP Benchy as a foundation. If you’re writing this along with us, be sure to install the latest WP Benchy plugin zip first.

Here’s what it looks like:

<?php
/*
* Plugin Name: Example Extension - Pressable Benchy
*/

require_once(ABSPATH . 'wp-content/plugins/wp-benchy/src/modules/Abstract_Module.php');

class Example_Benchy_Module extends Pressable\WP_Benchy\Modules\Abstract_Module {

    protected $id = 'example_benchy_module';

    public function run() {
        $this->checkpoint_hit('getting_posts');
        $posts = get_posts();
        $this->checkpoint_hit('got_posts');
    }
}

add_filter( 'pressable_benchy_register_modules', function( $modules ) {
    $modules['example_benchy_module'] = new Example_Benchy_Module();
    return $modules;
}, 10, 1);

Yup, that’s the entire plugin.

So simple that you can make a million of them, test all the things, and share them with the world.

Testing with Simplicity

Let’s walk through how it works alongside the Benchy plugin.

First, we define a class that extends the base class from Benchy, adding in a few items:

require_once(ABSPATH . 'wp-content/plugins/wp-benchy/src/modules/Abstract_Module.php');

class Example_Benchy_Module extends Pressable\WP_Benchy\Modules\Abstract_Module {

    protected $id = 'example_benchy_module';

    public function run() {
        ...
    }

}

This allows Benchy to do the heavy lifting for us. Behind the scenes, it’s doing things like:

  • Handling results
  • Registering REST API endpoints
  • Including the test in the Benchy UI
  • Managing “checkpoints” (we’ll go over that in a minute)

With that out of the way, let’s look at what’s happening in the test itself. The “meat and potatoes” of the module is everything inside of the run() method:

public function run() {
    $this->checkpoint_hit('getting_posts');
    $posts = get_posts();
    $this->checkpoint_hit('got_posts');
}

All we’re doing is here getting some posts and looping through them, registering checkpoint hits along the way. Simple, eh?

Remember what we said earlier about taking timings at different points? That’s exactly what we’re doing here by calling the checkpoint_hit() method. Each time this is called, it takes the timing, then stores it in the object instance for later use.

Finally, there’s a filter being called like so:

add_filter( 'pressable_benchy_register_modules', function( $modules ) {
    $modules['example_benchy_module'] = new Example_Benchy_Module();
    return $modules;
}, 10, 1);

The entire purpose of this is to create an instance of the class and add it to the runner that will handle all of the heavy lifting when needed.

Easy Results Through Automatically Registered REST Routes

In the end, you have a new REST API endpoint at /wp-json/wp-benchy/v1/module/example_benchy_module that outputs results like this:

{
  "checkpoints": {
    "getting_posts_0": 762393698200307,
    "got_posts_0": 762393698363932
  },
  "first": 762393698200307,
  "last": 762393698363932,
  "total": 0.16363
}

As you can see here, we now have:

  • Timestamps (in nanoseconds) of each checkpoint.
  • Timestamps for the first and last checkpoints.
  • The total time (in milliseconds) that it took the entire test to complete.

Making the Test Data Useful

Yay! We have data! And it’s available through the REST API! Now what? Time to make it useful.

From here, we have 2 options:

  • Use a 3rd party load testing tool like k6 to pound our endpoints into oblivion.
  • Use JavaScript on the client side to hit those API endpoints and process the results from inside of the WordPress admin.

The second option is the most interesting.

Because we can initiate multiple client side requests from within the WordPress admin, this makes performing simple benchmarks accessible to everyone, without any additional tools of configuration.

Running WordPress Benchmarks, From Inside WordPress

Any modules, whether built into the main Benchy plugin or included as an extension, are automatically made available from within the WordPress admin. This means that even the most basic of users can gain fresh insights on how these tests are performing.

Screenshot of the WP Benchy plugin settings screen

Is it fancy? No. But it’s a utility – it gets the job done.

Ain’t nobody got time to waste making a pretty UI. Well, you might. I don’t. If you do, make it fancier – PRs are welcome.

Benchmarks For All

Now that the foundations are now in place, go write up some tests. Share your results with your friends, get them to share their tests with you. While you’re at it, share your tests with us too.

Tests are far from complete. We would love your contributions, whether it be integrating tests in your existing plugins, providing feedback, or even better – contributing tests to the main plugin for everyone to use.

We’d also love to know what you think about our approach, and what you would do to improve it. Together, we can provide everyone with actionable, repeatable data that helps all skill levels with reliable test metrics.

If you have any thoughts, we’d love to hear them. Reach out on the GitHub repo or tag us on Twitter.

Jeff Matson

Jeff Matson is a distinguished figure in the world of WordPress development, public speaking, and marketing with over 15 years of experience. As a seasoned software developer, programmer and engineer, he seamlessly integrates technical proficiency with a strong understanding of SEO and content marketing. Jeff is a prominent and engaged member of the WordPress community, and his contributions to the WordPress core reflect his commitment to open source software along with his talent for creating compelling marketing strategies has established him as a thought leader, by bridging the gap between technical intricacies and effective communication. In his spare time, Jeff loves to restore classic arcade machines and make whatever ridiculous project pops into his mad scientist brain.

Related blog articles