When someone opens a page on your WordPress site, a lot happens before the browser gets the first byte: the server starts PHP, WordPress loads the core, every active plugin and the theme, queries the database and builds the HTML. Every step costs time, and usually it’s one or two of them that make a page slow.
Here we follow one request all the way through and point out where the time typically goes. Where a topic deserves more space, we link to the guide that goes into depth.
From click to finished page
This is the journey for an ordinary page view that doesn’t hit a cache:
- The browser looks up the domain and sends an HTTP request to the server. With HTTP/3, the connection comes up faster, especially on mobile networks.
- The web server, usually Nginx or Apache, can’t build the page itself and passes the request on to PHP.
- PHP runs WordPress, which loads the configuration, core, plugins and theme.
- WordPress reads the URL and works out which content was requested.
- The main query and a number of smaller queries fetch the content from the database.
- The theme picks a template and builds the HTML, while plugins hook their own things in along the way.
- The finished HTML is sent back, and the browser starts fetching images, CSS and JavaScript.
Steps 2 to 6 make up most of the server’s response time, the time before the browser gets the first byte (TTFB). Step 7 is the browser’s work. The rest of the guide goes through the steps in the same order.
The web server and PHP
The web server is fast at what it can do itself: it sends files like images and stylesheets directly. Anything that has to be generated goes on to PHP, usually through PHP-FPM, which keeps a number of PHP processes (workers) ready. Each worker handles one request at a time. If they’re all busy, the next ones wait in line, and that’s where a site that feels fast with one visitor becomes slow during a campaign.
Two things on the server set the floor for how fast WordPress can get. One is the PHP version, because newer versions run the same code faster. The other is OPcache, which stores PHP files in compiled form, so hundreds of files from the core and plugins don’t have to be interpreted from scratch on every request. Both depend on your choice of hosting, and in the guide to PHP optimization in WordPress we go into depth on versions, OPcache and workers.
Bootstrap: what WordPress loads on every request
When PHP starts WordPress, it always follows the same path. index.php calls wp-blog-header.php, which reads wp-config.php through wp-load.php. Then wp-settings.php starts the rest: the core functions, your must-use plugins, all active plugins, the theme’s functions.php and finally the init hook, where most plugins get ready.
The key point is that this happens on every single request that doesn’t hit a cache. A plugin you only use on the contact page is still loaded when someone opens a product page, and if it runs database queries or calls an external API as early as init, every visitor pays for it. That’s why the number of plugins doesn’t say much on its own; what counts is what they do when they load. We’ve written about a concrete example: a plugin that spent 100 ms on JSON on the category pages of an online store, because an old function processed text character by character.
The folder structure follows the same logic. wp-includes is the core, wp-admin is the admin area, which is only loaded when someone is in the dashboard, and wp-content is yours: theme, plugins and uploads. Performance problems almost always live in wp-content, because that’s where the third-party code is.
wp-config.php holds more than the database credentials. Among other things, this is where you set how much memory WordPress may use:
define( 'WP_MEMORY_LIMIT', '256M' ); // front end
define( 'WP_MAX_MEMORY_LIMIT', '512M' ); // admin area
The limit can’t exceed what the server allows, and a higher limit doesn’t make the site faster. It just stops heavy pages from failing.
Hooks: where plugins attach
Plugins and themes don’t change the WordPress core. They attach to hooks, fixed points in the code where WordPress asks whether anyone wants to do something. An action is a moment in time, such as wp_head, when the page head is built, or save_post, when a post is saved. A filter is a value that’s passed past everyone who has signed up and can be changed along the way:
add_filter( 'the_content', function ( $content ) {
return $content . '<p>Thanks for reading.</p>';
} );
That makes WordPress flexible, but it also makes the cost invisible. the_content can have ten callbacks from five different plugins, and they all run every time the content is shown. A filter that runs a database query is called for every product in a list. If you want to see what’s actually attached to a page, Query Monitor shows both the hooks and the plugins behind them.
The database: where the time often goes
Once WordPress has read the URL, it builds the main query with WP_Query: the one that fetches the post, page or list of products the URL points to. Around it come a number of smaller queries from the core, the theme and plugins. On a simple blog page there are few. On an online store with filters and lots of plugins, they can add up to hundreds.
The data model explains a lot. All content, from posts to menu items, lives in wp_posts, and all extra fields live in wp_postmeta as key-value pairs. That’s flexible, but the values aren’t indexed, so a filter on a field’s value can end up reading the whole table. Those are the queries that make product filters slow in large stores. For the same reason, WooCommerce has moved orders out of wp_posts and into their own tables (High-Performance Order Storage), which is the default in new stores.
The third table to know is wp_options. Every setting marked for autoloading is fetched in one go on every request, so plugins that store large amounts of data there make every page a little heavier. You can check the size like this (the table prefix isn’t always wp_):
SELECT ROUND(SUM(LENGTH(option_value)) / 1024) AS kb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto-on', 'auto');
If the figure runs to several megabytes, it’s worth finding out which plugins are behind it. How to find and fix the heavy queries is covered in the guide to a faster WordPress database.
Template and output
Once the data is fetched, WordPress picks a template according to the template hierarchy: the most specific one that exists for that kind of content, such as single-product.php before single.php before index.php. With block themes and page builders like Oxygen, the principle is the same, but the templates live in the database instead of in files. Along the way, the theme and plugins register their stylesheets and scripts, and WordPress writes them into the page’s head and footer. This is where a page picks up lots of extra files if every plugin loads its files everywhere. What the browser then does with them is the subject of the guide to images, CSS and JavaScript.
Caching: the layers that skip work
Caching means some work is done once and reused. Each layer saves a different part of the journey:
| Layer | Stores | Saves | Limitation |
|---|---|---|---|
| Page cache | The finished HTML | PHP, WordPress and the database entirely | Doesn’t work on pages that differ for each visitor: cart, checkout, my account |
| Object cache | Results of queries and calculations | Repeated database lookups | Only remembers between requests with Redis or Memcached |
| Transients | Temporary data with an expiry time, such as responses from an API | External calls and heavy calculations | Stored in the database if there’s no object cache |
| OPcache | Compiled PHP code | Interpreting the PHP files | Enabled on the server, not in WordPress |
| Browser and CDN | Images, CSS and JavaScript | Fetching the files again | Doesn’t help the HTML of dynamic pages |
For a blog, the page cache is almost everything: most visitors get a finished HTML file, and PHP doesn’t run at all. An online store is different. Cart and checkout can’t be page-cached, and neither can the rest of the site once the customer has added something to the cart, so exactly the requests that make money go all the way through WordPress. This is where the object cache makes the difference, because it takes the load off the database on each of them. Which plugin connects WordPress to Redis matters too; see our benchmark of object cache plugins.
Admin-ajax, the REST API and WP-Cron
Not every request is a page view. admin-ajax.php is WordPress’s old route for dynamic calls from the browser, and every call starts all of WordPress, including the admin part. The Heartbeat API uses it to send a call at fixed intervals for as long as someone has the dashboard open. The REST API is the newer route: it also starts WordPress, but doesn’t load the admin area, and responses can be cached.
WP-Cron isn’t a real cron. Scheduled tasks are started when someone visits the site, so on a site with little traffic they run late, and on a busy site they’re checked constantly. It’s better to disable it and let the server run it on a fixed schedule:
// wp-config.php
define( 'DISABLE_WP_CRON', true );
# server crontab, every 5 minutes
*/5 * * * * curl -s https://example.com/wp-cron.php > /dev/null
How to find the bottleneck
Measure before you change anything. The server’s response time on an uncached page tells you whether the problem is on the server or in the browser. If it’s high, install Query Monitor, preferably on a copy of the site: it shows the number and duration of database queries, which plugins are behind them, hooks, and calls to external services. When that isn’t enough, a profiler like New Relic, Tideways or Blackfire shows which PHP functions the time goes into, request by request. That’s how we found the JSON function in the example above. Fix one thing at a time, and measure again.
Frequently asked questions
Do lots of plugins make WordPress slow?
Not in themselves. A small plugin that only registers a shortcode costs almost nothing, while a single heavy plugin can account for most of the response time. Measure what each plugin does instead of counting them.
Do I need Redis?
For a blog with a page cache, rarely. For an online store, almost always, because cart and checkout can’t be page-cached, and the object cache is what relieves the database on exactly those pages. Many hosting providers can turn on Redis for you.
Should I optimize or buy better hosting?
Measure first. If response time is high even on a page with few queries, or the server lacks OPcache and workers, it’s the hosting. If the time goes into the database or one plugin, a bigger server helps only a little, and it’s the code that needs fixing.
Want help?
We find where the time goes, from the server and database to plugins and the front end, and fix it without taking the store down. Read more about optimizing your WooCommerce store.