DEV Community

Cover image for WordPress Feels Slow? Profile Plugins and Queries Before You Buy More Hosting
AumCreate
AumCreate

Posted on

WordPress Feels Slow? Profile Plugins and Queries Before You Buy More Hosting

A slow WordPress page is not automatically a hosting problem. Before you upgrade, split the time into three buckets: PHP/plugin execution, database query time, and network time to first byte. If query time is low and PHP time is high, a bigger database does not help. If query time is high, a faster CPU can hide a missing index for a while. Profile before you upgrade.

Step 1: Measure a stable baseline

Use a staging copy with the same plugins and data. Measure the uncached page for profiling, then measure the cached page separately.

URL="https://staging.example.com/"

for i in 1 2 3 4 5; do
  curl -s -o /dev/null -w '%{time_starttransfer}' "$URL"; echo
done
Enter fullscreen mode Exit fullscreen mode

Do not trust one run. Use the median of several runs. If the page is cached, the uncached number is the one that tells you about plugins and queries.

Step 2: Log page time, query count, and query time

Add this to wp-config.php while profiling:

define('SAVEQUERIES', true);
Enter fullscreen mode Exit fullscreen mode

Create wp-content/mu-plugins/wp-profile.php:

<?php
add_action('shutdown', function () {
    if (!defined('SAVEQUERIES') || !SAVEQUERIES) {
        return;
    }

    global $wpdb;

    $total = 0.0;
    $slow = [];

    foreach ((array) $wpdb->queries as $q) {
        $seconds = (float) $q[1];
        $total += $seconds;

        if ($seconds >= 0.05) {
            $slow[] = [
                'sql' => $q[0],
                'seconds' => round($seconds, 4),
                'caller' => $q[2],
            ];
        }
    }

    error_log(sprintf(
        'WP_PROFILE page=%ss query=%ss queries=%d slow=%s',
        timer_stop(0),
        round($total, 4),
        count($wpdb->queries),
        wp_json_encode($slow)
    ));
});
Enter fullscreen mode Exit fullscreen mode

Load the page, then read the PHP error log. Remove the constant and the mu-plugin after profiling. SAVEQUERIES adds overhead and should not stay on in production.

Step 3: Sweep plugins on staging

Do not do this on production. Deactivate all active plugins, then activate one at a time and measure the same URL.

cd /path/to/wordpress

measure() {
  for i in 1 2 3; do
    curl -s -o /dev/null -w '%{time_starttransfer}' "$1"; echo
  done | sort -n | sed -n '2p'
}

wp plugin list --status=active --field=name > /tmp/active-plugins.txt
wp plugin deactivate --all

while read -r plugin; do
  wp plugin activate "$plugin"
  echo "$plugin $(measure https://staging.example.com/)"
  wp plugin deactivate "$plugin"
done < /tmp/active-plugins.txt

wp plugin activate $(cat /tmp/active-plugins.txt)
Enter fullscreen mode Exit fullscreen mode

Skip plugins that the site needs to boot. Also test with the default theme if you suspect the theme. A plugin that adds a small query can still add a large external HTTP call on every page load.

Step 4: Read the slow queries

Open the log line that starts with WP_PROFILE. Take the SQL from the slow array and run EXPLAIN in MySQL:

EXPLAIN SELECT post_id
FROM wp_postmeta
WHERE meta_key = 'example_key';
Enter fullscreen mode Exit fullscreen mode

Look for:

  • type=ALL on a large table
  • a large rows estimate
  • Using temporary
  • Using filesort

Add an index only after you check the real query pattern and test write performance. An index that helps one report can hurt checkout or editorial saves.

Step 5: Check autoloaded options

Large autoloaded options are loaded on every request. Find the largest ones:

SELECT option_name, LENGTH(option_value) AS bytes
FROM wp_options
WHERE autoload = 'yes'
ORDER BY bytes DESC
LIMIT 20;
Enter fullscreen mode Exit fullscreen mode

Do not delete rows blindly. Identify stale options from plugins you no longer use, then remove them through the plugin or a backup-backed cleanup.

Step 6: Check transients

Expired transients can pile up in wp_options. List the largest:

SELECT option_name, LENGTH(option_value) AS bytes
FROM wp_options
WHERE option_name LIKE '%transient%'
ORDER BY bytes DESC
LIMIT 20;
Enter fullscreen mode Exit fullscreen mode

If you see many expired transients, clean them from the database or with a maintenance routine you control.

Step 7: Decide what to buy

  • Query time high, page time high: fix queries, indexes, or autoloaded data before buying a bigger database.
  • Query time low, page time high: profile plugins, theme hooks, external HTTP calls, and PHP workers.
  • Time to first byte high before WordPress runs: look at DNS, TLS, network, and host-level caching.
  • Only logged-in admin pages are slow: profile admin hooks, not the front end.

Hosting upgrades are useful when the profile shows a real resource limit. They are a poor substitute for a missing index or a plugin that calls a remote API on every request.

AumCreate is a one-person WordPress shop in Singapore. It does custom WordPress development and deployment setup. If you want help turning this profile into a fix, the services page is here: AumCreate services.

Top comments (0)