Skip to main content Skip to content

PHP optimization in WordPress

When a page doesn’t hit a cache, the server’s time goes to two things: the database and PHP. If the database is in order and the page is still slow before the browser gets anything at all, it’s PHP that needs looking at: the version that runs the code, the way the server keeps PHP ready, and the code your plugins and theme actually run.

This guide builds on the WordPress architecture, so if bootstrap and hooks are new concepts, start there.

The PHP version

The cheapest improvement is almost always running a newer PHP version. Every major version has made the same code faster, and older versions stop getting security updates; you can see which are still supported on php.net. You’ll find your current version under Tools → Site Health in WordPress.

The risk lies in old plugins and themes that haven’t been updated for the new version. So switch on a copy of the site first, turn on WP_DEBUG_LOG, click through the most important pages and an order, and read the log file. If your provider can’t give you a newer version or a copy to test on, that’s a sign you should look for hosting with the right PHP setup.

OPcache

PHP is an interpreted language: every file has to be read and compiled before it can run. OPcache keeps the compiled code in memory, so that only happens once. For WordPress, which loads hundreds of files from the core, plugins and theme on every request, it’s the single setting that matters most. It’s turned on on most servers, but the default sizes are often too small for an online store with lots of plugins:

opcache.enable = 1
opcache.memory_consumption = 256      ; MB for compiled code
opcache.interned_strings_buffer = 16  ; MB for repeated strings
opcache.max_accelerated_files = 20000 ; more files than WordPress + plugins
opcache.validate_timestamps = 1
opcache.revalidate_freq = 60          ; check for changed files every minute

You can see whether the size fits with opcache_get_status() or a small plugin that shows it: if the memory is full, or the number of files is reached, OPcache throws code out and compiles it again. If you set validate_timestamps to 0, you also save the check, but then OPcache has to be cleared on every update, or the old code keeps running. The JIT compiler from PHP 8 helps computation-heavy code, but WordPress spends most of its time waiting for the database and building text, so the gain is usually small.

OPcache mustn’t be confused with an object cache. OPcache remembers the code; an object cache remembers data the code would otherwise fetch from the database. You need both, and which plugin you use to connect the object cache can make a difference, as our test of object cache plugins shows.

PHP-FPM and the number of workers

On most servers, PHP runs through PHP-FPM, which keeps a number of PHP processes, workers, ready. Each worker handles one request at a time. If a page takes 300 milliseconds to generate, one worker can handle just over three of them per second. If more requests arrive than there are free workers, they wait in line, and response time rises for everyone, even though each page is just as fast as before.

The natural answer is more workers, but each worker uses memory, and a WordPress process with WooCommerce typically uses well over 100 MB. If you set the limit higher than the memory can carry, the server starts swapping, and then everything slows down. A useful rule of thumb is: the memory available to PHP divided by what a worker uses on average. You can see the usage with ps or in your monitoring once the site has run for a while under normal traffic.

; /etc/php/8.x/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 20      ; the ceiling: memory for PHP / memory per worker
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 8
pm.max_requests = 500     ; restart a worker after 500 requests
pm.status_path = /fpm-status

The status page shows how many workers are busy, and whether requests have been waiting in line (listen queue and max children reached). If the ceiling is often reached, you either need more workers and more memory, or to make each request faster, so workers are freed sooner. The latter is usually cheaper. The web server and PHP-FPM should talk over a Unix socket when they’re on the same server; that saves the network layer.

Memory and time limits

memory_limit in PHP and WP_MEMORY_LIMIT in wp-config.php decide how much a single request may use, and max_execution_time how long it may run. They don’t make anything faster. They stop a heavy import or report from failing, and they cap how much damage a runaway request can do. 256 MB for the front end is plenty for most sites. If you need much more, it’s worth finding out why.

Find the slow code

The server settings set the floor. The rest is the code, and it has to be measured, not guessed. Query Monitor is the place to start: it shows database queries, calls to external services and which plugins are behind them, and it shows the total time and memory for the page. If it’s the queries that take the time, the next step is about the database.

If it isn’t the queries, you need a profiler that shows which PHP functions the time goes into. New Relic and Tideways do it continuously in production; Blackfire, SPX and Xdebug are good for looking at one request in detail. That’s how we found the problem in our JSON case: a function in a filter plugin that spent 100 ms on something PHP can do itself in under a millisecond.

What we find most often

When we profile slow WordPress sites, it’s rarely anything exotic. It’s usually one of these:

  • Calls to an external API on every request, such as license checks or exchange rates, without storing the response in a transient.
  • Queries inside a loop, so a product list with 24 products causes 24 extra lookups.
  • Heavy work on init or plugins_loaded that should only run in the admin or on one specific page.
  • Old code that reinvents something PHP has built in, like the JSON function above.
  • Background jobs from WP-Cron or Action Scheduler running during an ordinary page view.

What they have in common is that the fix is rarely more hardware. It’s getting the code to stop doing the work, or to do it once and remember the result.

Load testing

Workers and memory have to match the traffic you get when it counts, such as a campaign. Test it on a copy of the site, not in production, with a tool like k6, and watch the FPM status page meanwhile:

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

export const options = { vus: 20, duration: '2m' };

export default function () {
    http.get('https://staging.example.com/product-category/shoes/');
    sleep(1);
}

Test pages that aren’t page-cached, such as categories with filters and the cart, because those are the ones that hit PHP.

Frequently asked questions

Can I optimize PHP on shared hosting?

Only what the provider lets you choose, usually the PHP version. They control workers and OPcache. You can still make the code faster, and that’s often where the biggest gain is.

Should I turn on JIT?

It rarely hurts, but don’t expect much. WordPress mostly waits for the database and builds text, and that’s not the kind of work JIT speeds up. Spend the memory on OPcache first.

How many workers should I have?

As many as the memory can carry, and no more. Measure what a worker uses, divide the memory available for PHP by that number, and watch whether the queue fills up.

Want help?

We profile your site, find the code and settings that cost time, and fix them. Read more about WooCommerce optimization.

← All articles

Want help with your store?

Is your store slow, unstable or just hard to work with? Write a few lines and a link, and you'll get an honest assessment from the developer.