<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adrian Wadowski</title>
    <description>The latest articles on DEV Community by Adrian Wadowski (@adrianwadowski).</description>
    <link>https://dev.to/adrianwadowski</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4207188%2F053b90b0-a78b-4864-89ff-4a152a9773a9.jpg</url>
      <title>DEV Community: Adrian Wadowski</title>
      <link>https://dev.to/adrianwadowski</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adrianwadowski"/>
    <language>en</language>
    <item>
      <title>Why WooCommerce Is Slow: 924 ms to Build a Page, 27 ms to Serve It</title>
      <dc:creator>Adrian Wadowski</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:13:29 +0000</pubDate>
      <link>https://dev.to/carthacker/why-woocommerce-is-slow-924-ms-to-build-a-page-27-ms-to-serve-it-3mi5</link>
      <guid>https://dev.to/carthacker/why-woocommerce-is-slow-924-ms-to-build-a-page-27-ms-to-serve-it-3mi5</guid>
      <description>&lt;p&gt;Scrub Daddy's WooCommerce server needs 924 milliseconds to build a single product page from scratch. Its shoppers get that page in 27. The difference is a cache, and it's the reason a slow WooCommerce store and a fast one can run on the same kind of server.&lt;/p&gt;

&lt;p&gt;Most WooCommerce stores don't get there. In August 2026, only 12.7% of WooCommerce sites got their first byte to phone visitors in 800 milliseconds or less, the line Google calls good, against 81.9% of Shopify stores, and the WooCommerce figure has barely moved in five years.&lt;/p&gt;

&lt;p&gt;Shopify keeps every store on its own servers, and on WooCommerce it's the owner who picks the server, the PHP settings and the cache. So &lt;strong&gt;who falls through the cache&lt;/strong&gt;, and what does it cost them?&lt;/p&gt;

&lt;p&gt;If your WooCommerce store is slow, here's the server side of it, measured. I built a test store and measured it one layer at a time, using WooCommerce 11.1 with 600 products, 1,200 variations and the free Storefront theme, on PHP 8.3, MariaDB 11.4 and nginx, in Docker on my own workstation.&lt;/p&gt;

&lt;p&gt;One product page took 473 milliseconds of server time without OPcache, 100 with it, and 1.1 from a page cache. Then I compared it with two real stores.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is WooCommerce slow?
&lt;/h2&gt;

&lt;p&gt;WooCommerce is slow when PHP has to build each page from the database while the shopper waits. Every request starts WordPress, WooCommerce, the theme and every active plugin, which on my test store meant loading 1,392 PHP files and running 90 database queries for one product page, and only then does the first byte go out.&lt;/p&gt;

&lt;p&gt;A page cache skips all of it, and that's why &lt;strong&gt;fast WooCommerce stores build pages rarely&lt;/strong&gt; and let a cache answer everyone else.&lt;/p&gt;

&lt;p&gt;Server response (Time to First Byte) counts everything before that first byte arrives: redirects, the DNS lookup, the connection and its encryption handshake, then the server's own work.&lt;/p&gt;

&lt;p&gt;Good is 800 milliseconds or less. It doesn't count as a Core Web Vital itself, yet Loading (Largest Contentful Paint) can't begin until it ends, and only 43.7% of WooCommerce sites pass Loading on phones, against 87.3% of Shopify stores. The full comparison is in &lt;a href="https://carthacker.com/trends/ecommerce-speed-statistics/" rel="noopener noreferrer"&gt;the speed data by platform&lt;/a&gt;, and the three scores themselves in &lt;a href="https://carthacker.com/customization/core-web-vitals/" rel="noopener noreferrer"&gt;the Core Web Vitals article&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Distance eats part of that budget too. A phone on a mobile network can use a few hundred milliseconds reaching a distant server and agreeing on encryption before your server starts any work, so the server itself doesn't get the whole 800 milliseconds. My benchmark measures only the server's own work, inside the same network, so read its numbers as the server's share of the wait.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Measure the server, cached and uncached
&lt;/h2&gt;

&lt;p&gt;You'll want two numbers from a product page: the server response a shopper gets, and the time your server needs when it has to build the page itself. The first decides whether you pass. The second shows how much your cache is hiding, and it's close to what every cart, checkout and account page costs, because WooCommerce never lets those be cached.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run the Store Speed Test on a product page: paste the address of your best-selling product. The report shows the real-visitor scores Google holds for that page, and it raises "Your server takes a long time to answer" when the first byte took longer than 800 milliseconds in the lab run.&lt;/li&gt;
&lt;li&gt;Open the same page in Chrome DevTools: in an Incognito window press F12, open the &lt;strong&gt;Network&lt;/strong&gt; tab and reload. The row whose Type reads document is the page itself, and its Time column is the whole request. Click it and open the &lt;strong&gt;Timing&lt;/strong&gt; tab to split that time: "Waiting for server response" is the server's share.&lt;/li&gt;
&lt;li&gt;Repeat it with a made-up query string: add &lt;code&gt;?nocache=12345&lt;/code&gt; to the address and change the number on every try. Most page caches have never stored that address, so the wait you see now is PHP building the page from scratch.&lt;/li&gt;
&lt;li&gt;Read the cache headers: on the same row, the Headers tab names the cache that answered. &lt;code&gt;cf-cache-status: HIT&lt;/code&gt; is Cloudflare, &lt;code&gt;x-litespeed-cache: hit&lt;/code&gt; is LiteSpeed Cache, and an &lt;code&gt;x-cache&lt;/code&gt; header reading HIT usually comes from your host's own cache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From a terminal, curl gives the same two numbers. The address is a placeholder, and you'll put your own product page there, and &lt;strong&gt;the first command is worth running twice&lt;/strong&gt;, because the first request may only fill the cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# What a shopper gets: the cached page&lt;/span&gt;
curl &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'first byte: %{time_starttransfer}s\n'&lt;/span&gt; https://shop.example.com/product/blue-mug/

&lt;span class="c"&gt;# What PHP needs: an address no cache has stored yet&lt;/span&gt;
curl &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'first byte: %{time_starttransfer}s\n'&lt;/span&gt; &lt;span class="s2"&gt;"https://shop.example.com/product/blue-mug/?nocache=&lt;/span&gt;&lt;span class="nv"&gt;$RANDOM&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I add a third request when I test any store, mine or a competitor's: the same address with a random gclid on it, like an ad click.&lt;/p&gt;

&lt;p&gt;The plain address is what shoppers get, and the nocache one shows the server without its cache (roughly what the cart and checkout cost). If the gclid one looks like the nocache one, every paid click waits for PHP, and that's a cache setting you can fix without a bigger server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Turn on OPcache and run PHP 8.3 or newer
&lt;/h2&gt;

&lt;p&gt;OPcache did more for PHP itself than any other setting in the test. PHP turns every file it runs into bytecode before executing it, and without OPcache it'll repeat that work on every request, for all 1,392 files a product page loads. With OPcache the compiled code stays in memory between requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On one CPU core the product page dropped from 473 to 100 milliseconds&lt;/strong&gt;, and the core served about 9 pages a second instead of 1.9.&lt;/p&gt;

&lt;p&gt;Most hosts switch OPcache on, but some cheap plans leave it off, and you won't find a word about it in the WordPress dashboard. In cPanel, open &lt;strong&gt;Select PHP Version&lt;/strong&gt; and look for opcache among the ticked extensions. On other hosts, ask support to confirm that opcache.enable is 1 for your site's PHP; the command line on the server usually has its own setting, so a check you run there won't prove anything about your pages.&lt;/p&gt;

&lt;p&gt;The PHP version doesn't matter as much for speed. PHP 8.3 built the same pages between 2% and 9% faster than PHP 7.4 on two cores (the biggest gain was on the variable product page, 123 against 135 milliseconds).&lt;/p&gt;

&lt;p&gt;Security is the stronger reason to upgrade: PHP 7.4 has had no security fixes since November 2022 and PHP 8.1 none since December 2025, and PHP 8.2 gets its last ones in December 2026. You'll find your version under &lt;strong&gt;WooCommerce &amp;gt; Status&lt;/strong&gt;. Move to 8.3 or 8.4 on a staging copy first, because it's usually an old plugin that breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Cache whole pages for shoppers who aren't logged in
&lt;/h2&gt;

&lt;p&gt;A page cache stores the finished HTML of a page and hands it to the next visitor without starting WordPress, and &lt;strong&gt;it moves the number more than anything else&lt;/strong&gt;. In the test, Cache Enabler, a small page cache plugin, answered the product page in 1.7 milliseconds and the nginx cache in front of PHP in 1.1, against 100 milliseconds for PHP with OPcache.&lt;/p&gt;

&lt;p&gt;With 20 shoppers asking at once, the nginx cache on one CPU core delivered 9,973 pages a second, and PHP with OPcache about 9.&lt;/p&gt;

&lt;p&gt;Where the cache sits decides how much work it skips. Cache Enabler, like WP Rocket on an nginx server, still starts PHP for every request and only skips WordPress. WP Rocket on Apache uses rewrite rules in .htaccess to send the stored file without PHP, LiteSpeed Cache is served by the LiteSpeed web server itself, and a host cache built on Varnish or nginx answers before PHP is involved at all.&lt;/p&gt;

&lt;p&gt;A CDN cache such as Cloudflare's goes one step further and answers from a data center near the shopper.&lt;/p&gt;

&lt;p&gt;Don't run more than one page cache. Two plugins that both cache pages, or a cache plugin on top of a host cache that already does the job, mostly add ways for a stale page to survive a purge.&lt;/p&gt;

&lt;p&gt;If your host caches pages, use its plugin to clear the cache and switch WP Rocket's page caching off; WP Rocket's other features (delaying JavaScript, optimizing CSS and images) work in the browser and can still be worth keeping.&lt;/p&gt;

&lt;p&gt;What a page cache can't answer is where the slow part of a WooCommerce store lives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cart, checkout and My Account pages. WooCommerce marks them as never cacheable, and every cache respects that. In the test the cart page took 133 milliseconds even while the nginx cache served every product page in about 1 millisecond.&lt;/li&gt;
&lt;li&gt;Logged-in customers. Most caches hand them pages built by PHP, and many also skip shoppers who have something in the cart, so your best customers get the slow version.&lt;/li&gt;
&lt;li&gt;The cart fragments request, the one in step 5.&lt;/li&gt;
&lt;li&gt;The first visitor after a purge. Saving a product, a stock change or a plugin setting can empty part of the cache, and the next shopper on each of those pages waits for PHP again.&lt;/li&gt;
&lt;li&gt;Security tokens older than a day, and addresses the cache has never seen: search results, filtered category pages and ad links with tracking parameters. The two real stores show both.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two fast stores, and the shoppers who fall through their cache
&lt;/h2&gt;

&lt;p&gt;Scrub Daddy and Van Leeuwen Ice Cream both sell on WooCommerce, and &lt;strong&gt;both serve their product pages faster than most Shopify stores do&lt;/strong&gt;. I measured one product page from each on October 4, 2026, from my own connection in Poland, three requests per test, with a script that takes the connection setup out of the server number, then opened both in Chrome DevTools.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjuwzc52ewaec9hco7us.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdjuwzc52ewaec9hco7us.jpg" alt="Chrome DevTools Network panel next to the Scrub Daddy x Harry's Smile! product page, requests sorted by size, the page document highlighted: status 200, type document, 94.0 kB, 37 ms; the status bar reads 260 requests, 3.7 MB transferred, 10.1 MB resources, DOMContentLoaded 494 ms, Load 647 ms" width="800" height="597"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Scrub Daddy product page document arrives in 37 ms, answered from Cloudflare's cache&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fha73jkgisi8km8v54rxp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fha73jkgisi8km8v54rxp.jpg" alt="Chrome DevTools Network panel next to the Van Leeuwen Ice Cream Mango Sticky Rice product page, the page document highlighted as the first request: status 200, type document, 23.5 kB, 30 ms; 106 requests, 2.3 MB transferred, 5.7 MB resources, DOMContentLoaded 618 ms, Load 1.14 s" width="800" height="601"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Van Leeuwen Ice Cream serves its cached product page document in 30 ms&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product page&lt;/th&gt;
&lt;th&gt;Scrub Daddy&lt;/th&gt;
&lt;th&gt;Van Leeuwen Ice Cream&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cached page, server wait&lt;/td&gt;
&lt;td&gt;27 ms&lt;/td&gt;
&lt;td&gt;21 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cached page in Chrome DevTools&lt;/td&gt;
&lt;td&gt;37 ms&lt;/td&gt;
&lt;td&gt;30 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Answered by&lt;/td&gt;
&lt;td&gt;Cloudflare's cache&lt;/td&gt;
&lt;td&gt;Cloudflare's cache, with a server cache behind it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same page built by PHP&lt;/td&gt;
&lt;td&gt;924 ms&lt;/td&gt;
&lt;td&gt;646 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ad click address (gclid, fbclid, utm_source)&lt;/td&gt;
&lt;td&gt;27 to 52 ms, still cached&lt;/td&gt;
&lt;td&gt;228 to 233 ms, past Cloudflare&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cart fragments on a first visit&lt;/td&gt;
&lt;td&gt;Sent, answered in 767 ms&lt;/td&gt;
&lt;td&gt;Not sent until the cart has an item&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Braintree payment setup request&lt;/td&gt;
&lt;td&gt;Refused with 403 after 0.5 to 0.8 s&lt;/td&gt;
&lt;td&gt;Not on the page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML of the page&lt;/td&gt;
&lt;td&gt;407 KB, 150 script tags&lt;/td&gt;
&lt;td&gt;119 KB, 71 script tags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theme&lt;/td&gt;
&lt;td&gt;Shoptimizer&lt;/td&gt;
&lt;td&gt;Their own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Plugins with files on the page&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Server wait is the time to the first byte minus the connection setup, the median of three requests; DevTools shows the whole request, download included, in its Time column. The cart fragments and Braintree rows come from a fresh Chrome profile with nothing in the cart.&lt;/p&gt;

&lt;p&gt;Neither store is fast at building pages. Scrub Daddy's server needs those 924 milliseconds because the page carries a premium theme, 15 plugins with files on the page and 407 KB of HTML, more than nine times what my one-core test store needs with OPcache.&lt;/p&gt;

&lt;p&gt;Hardly any shopper pays for it, though. Cloudflare holds a copy of the page, and every request I made, including the ones from ad links, got that copy in under 60 milliseconds.&lt;/p&gt;
&lt;h3&gt;
  
  
  The copy that was 36 hours old
&lt;/h3&gt;

&lt;p&gt;That long cache has a price. The copy of the product page Cloudflare handed me was 130,315 seconds old, about 36 hours, and the page carries a WordPress security token, a nonce, that the Braintree payment plugin sends back when it sets up the payment buttons, and WordPress won't accept a nonce that's older than 24 hours.&lt;/p&gt;

&lt;p&gt;On every load I recorded, &lt;strong&gt;that request came back 403 Forbidden&lt;/strong&gt; after 0.5 to 0.8 seconds of PHP, with the answer WordPress gives an expired or invalid nonce: Cookie check failed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F22x8dzghadjx1zoy9gvu.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F22x8dzghadjx1zoy9gvu.jpg" alt="Chrome DevTools Network panel next to the Scrub Daddy product page, requests sorted by time, the slowest one highlighted at the top: ?wc-ajax=wc_braintree_frontend_request, status 403, type xhr, 1.0 kB, 544 ms; 264 requests, 4.0 MB transferred, DOMContentLoaded 486 ms, Load 656 ms" width="800" height="597"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The slowest request on the cached Scrub Daddy page is the Braintree setup call, and the server refuses it: 403 after 544 ms&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;a page that carries a nonce should leave the cache within 12 hours&lt;/strong&gt;, the shortest time WordPress guarantees a nonce stays valid, and that's why WP Rocket's default cache lifespan is 10 hours.&lt;/p&gt;

&lt;p&gt;Scrub Daddy's Cloudflare rule keeps the page for a year, an s-maxage of 31,536,000 seconds, and that's fine for a page with no tokens. With one, the page needs a purge at least twice a day, or the plugin has to fetch its token from a request no cache stores.&lt;/p&gt;

&lt;p&gt;Want to know whether a page carries one? Open its source with Ctrl+U and search for nonce.&lt;/p&gt;
&lt;h3&gt;
  
  
  The ad click that misses the cache
&lt;/h3&gt;

&lt;p&gt;Every ad click arrives with a unique tag on the address: gclid from Google Ads, fbclid from Meta, utm_source and its siblings from newsletters and campaigns. To a cache that stores pages by their full address, each tagged address is a new page, so &lt;strong&gt;the visitor you've paid for is the one who waits&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ac3aco7vr8ivnlhb3w4.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4ac3aco7vr8ivnlhb3w4.jpg" alt="Chrome DevTools Network panel next to the Van Leeuwen Mango Sticky Rice page opened with a gclid parameter, requests sorted by time, the page document highlighted: mango-sticky-rice/?gclid=carthacker-test, status 200, type document, 23.6 kB, 232 ms; 105 requests, 2.3 MB transferred, DOMContentLoaded 622 ms, Load 1.08 s" width="800" height="597"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Opened from an ad link, the same Van Leeuwen page is a new address to Cloudflare because of the gclid, and the document takes 232 ms instead of 30&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Van Leeuwen shows the cost. With a fresh gclid its product page missed Cloudflare, and the document took 232 milliseconds in DevTools against 30 for the plain address (my measured server wait was about 230 against 21).&lt;/p&gt;

&lt;p&gt;The server cache behind Cloudflare caught it, and a store without that second layer would've built the page in PHP, which for this page takes about 650 milliseconds. Scrub Daddy's Cloudflare cache doesn't care about the parameters, and its ad visitors got the same 27 to 52 milliseconds as everyone else.&lt;/p&gt;

&lt;p&gt;It's a setting you can fix. WP Rocket ignores a long list of these parameters by default and serves the cached page. In LiteSpeed Cache, add them to &lt;strong&gt;Drop Query String&lt;/strong&gt; on the Cache tab; fbclid, gclid and utm* cover the common ones.&lt;/p&gt;

&lt;p&gt;On Cloudflare, check what your cache rule does with query strings, because by default the full address, query string included, is the cache key, and the options to leave single parameters out depend on your plan.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4: Match PHP workers to CPU cores
&lt;/h2&gt;

&lt;p&gt;A PHP worker builds one page. Managed WordPress plans sell them as PHP workers or PHP threads, and a store builds as many pages at once as it has workers with a free CPU core under them. When every worker is busy, the next request waits in a queue, and that's where an uncached page turns from slow into unusable.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6nif4gl963x0pd3cl52x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6nif4gl963x0pd3cl52x.png" alt="Bar chart of the median server response for a WooCommerce product page with 20 shoppers asking at once: 1 core without OPcache 10.4 s at 1.9 pages a second, 1 core with OPcache 2.0 s at 8.7, 2 cores 1.0 s at 21, 4 cores 0.5 s at 39, and 1 core behind a server page cache 0.6 ms at 9,973 pages a second" width="800" height="432"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;With twenty shoppers at once on an uncached product page, the wait falls with OPcache and with every added CPU core, and a page cache removes it&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I sent 20 simultaneous shoppers at the uncached product page for 20 seconds per setup. On one core without OPcache the median shopper waited 10.4 seconds, because the core could finish only 1.9 pages a second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OPcache on the same core cut the wait to 2.0 seconds&lt;/strong&gt;, two cores to 1.0 second and four cores to 0.5 seconds, as throughput rose from 9 to 21 and then 39 pages a second. From the nginx cache it was 0.6 milliseconds.&lt;/p&gt;

&lt;p&gt;I'd expected more pages from more workers on one core, and I got fewer:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;PHP workers on one core&lt;/th&gt;
&lt;th&gt;Pages a second&lt;/th&gt;
&lt;th&gt;Median wait&lt;/th&gt;
&lt;th&gt;Memory the workers held&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;10.3&lt;/td&gt;
&lt;td&gt;1.8 s&lt;/td&gt;
&lt;td&gt;93 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;9.3&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;2.6 s&lt;/td&gt;
&lt;td&gt;467 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A worker without a free core only waits for one, and the core then switches between more half-built pages, so each one finishes later.&lt;/p&gt;

&lt;p&gt;Count before you buy: pages a second is roughly the number of CPU cores divided by the server time of one page. One core and a 100 millisecond page make about 9 to 10 pages a second, and a page that grows to 232 milliseconds with 15 plugins cuts that by more than half.&lt;/p&gt;

&lt;p&gt;How many workers, then? &lt;strong&gt;About two per CPU core.&lt;/strong&gt; Beyond that, workers add memory and a longer queue, so a plan that's selling many workers on a few cores is really selling you a longer queue. A store that serves most pages from a cache needs few, because only the cart, checkout, account pages and cart fragments reach PHP.&lt;/p&gt;

&lt;p&gt;And it's rarely memory that runs out: hosting panels and the top command showed 85 MB per worker (the resident figure, RSS), but most of that is OPcache and libraries every worker shares. When I read the proportional figure (Pss) from /proc//smaps_rollup, which splits shared memory between the processes that share it, it was 31 MB per worker, and each worker beyond eight added about 12 MB.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5: Stop the cart fragments request where nothing needs it
&lt;/h2&gt;

&lt;p&gt;Cart fragments is the script that keeps the cart total in a store's header up to date. It sends a request to &lt;code&gt;?wc-ajax=get_refreshed_fragments&lt;/code&gt;, which no page cache answers, on the first page a shopper opens in each browser tab and again after every cart change. Since WooCommerce 7.8 the script loads only where the classic Cart widget is shown, but many themes still load it everywhere.&lt;/p&gt;

&lt;p&gt;Storefront 4.6.2, WooCommerce's own theme, loads it on every page, and so does Scrub Daddy, whose product page sent the request on a first visit in a fresh Chrome profile and waited 767 milliseconds for the answer. Van Leeuwen's page loads the script only once the shopper has put something in the cart.&lt;/p&gt;

&lt;p&gt;Each request is a full WordPress start. On the test store it took 40 milliseconds of server time, small on its own, but twenty at once on one core took 648 milliseconds each and the core topped out at 32 a second.&lt;/p&gt;

&lt;p&gt;The request doesn't hold up the first paint, because the browser sends it after the page has loaded. &lt;strong&gt;It spends the PHP workers that the cart and checkout need&lt;/strong&gt;, and it's doing that just as a campaign brings a crowd.&lt;/p&gt;

&lt;p&gt;I nearly missed it myself. When I took the screenshots, I filtered the Scrub Daddy page by wc-ajax and found no cart fragments request at all. That was my mistake: I'd reloaded the page in the same tab a few times by then, and the script keeps the fragments in the tab's sessionStorage and skips the request while they're fresh, so after the first view there's nothing to see.&lt;/p&gt;

&lt;p&gt;A fresh browser profile showed it at once, one POST, 767 milliseconds. Test this one in a new window, and look before you reload.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fedbcql20e8ra7ksq2bre.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fedbcql20e8ra7ksq2bre.jpg" alt="Chrome DevTools Network panel filtered by wc-ajax next to the Scrub Daddy product page after several reloads in one tab: the only request is ?wc-ajax=wc_braintree_frontend_request, status 403, type xhr, 1.0 kB, 567 ms; no get_refreshed_fragments request" width="800" height="597"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;After a few reloads in one tab there's no cart fragments request, because the tab already holds the fragments, and the one wc-ajax request left is the Braintree call, refused with 403&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are three ways to stop it, from cleanest to quickest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use the Mini-Cart block: WooCommerce's block version of the header cart reads the cart without cart fragments, and block themes offer it in the Site Editor.&lt;/li&gt;
&lt;li&gt;Switch it off in your optimization plugin: Perfmatters, for one, has a &lt;strong&gt;Disable Cart Fragmentation&lt;/strong&gt; setting in its WooCommerce options.&lt;/li&gt;
&lt;li&gt;Hold it back with a snippet. This one keeps cart fragments on the page but withholds its address until the browser finds the woocommerce_cart_hash cookie, which WooCommerce sets when something goes into the cart. Add it with the free WPCode plugin or your child theme's functions.php.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Load WooCommerce cart fragments only for shoppers who have something in the cart.&lt;/span&gt;
&lt;span class="c1"&gt;// The check runs in the browser, so it also works on pages served from a page cache.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'script_loader_tag'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$handle&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wc-cart-fragments'&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$handle&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;is_cart&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;is_checkout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;'LOADER'
&amp;lt;script&amp;gt;
(function () {
    function load() {
        var held = document.querySelector('script[data-cart-src]');
        if (!held || !/(^|;\s*)woocommerce_cart_hash=/.test(document.cookie)) return;
        var s = document.createElement('script');
        s.src = held.getAttribute('data-cart-src');
        held.removeAttribute('data-cart-src');
        document.head.appendChild(s);
    }
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', load);
    } else {
        load();
    }
    document.addEventListener('click', function () { setTimeout(load, 1000); });
})();
&amp;lt;/script&amp;gt;
LOADER;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;' src='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;' data-cart-src='&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$loader&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The decision happens in the shopper's browser, so it'll hold on pages served from any cache, and Van Leeuwen's own version does the same in a few lines. On the test store, Storefront as shipped sent the request on a first visit with an empty cart; with the snippet that visit sent nothing, Add to cart sent it once, and the header showed 1 item.&lt;/p&gt;

&lt;p&gt;Check it yourself: in a fresh Incognito window, type wc-ajax into the Network filter, open a product page and expect nothing, then add a product and expect one get_refreshed_fragments request.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 6: Add Redis object caching for the uncacheable pages
&lt;/h2&gt;

&lt;p&gt;An object cache keeps the results of database queries in memory between requests, so the next page can skip them. WordPress has one built in that forgets everything when the request ends; Redis Object Cache, the most widely used plugin for a persistent one, stores the results in Redis, which your host has to run for you.&lt;/p&gt;

&lt;p&gt;On the test store it did what it promises and cut the database queries for a product page from 90 to 26, and &lt;strong&gt;the page didn't get any faster&lt;/strong&gt;: 103 milliseconds with Redis against 100 without, and the cart 128 against 132. A database on the same machine with every row in memory answers those 64 queries in a few milliseconds anyway.&lt;/p&gt;

&lt;p&gt;So does Redis make WooCommerce faster? It pays off on a slow database. With the 15-plugin stack the product page ran 292 queries, Redis cut them to 62, and the page went from 232 to 213 milliseconds, about 8% faster.&lt;/p&gt;

&lt;p&gt;The client made little difference: PhpRedis, the compiled extension, and Predis, the fallback written in PHP that the plugin uses when the server lacks the extension, finished within 2 milliseconds of each other on every page I measured. When the database runs on another server, every query becomes a network round trip, and that's the store Redis helps most.&lt;/p&gt;

&lt;p&gt;So use it for the pages a page cache can't answer: the cart, the checkout, My Account, logged-in customers and the admin, which are also the pages where a slow server stands between a shopper and the order.&lt;/p&gt;

&lt;p&gt;Install Redis Object Cache only if your host provides Redis, then open &lt;strong&gt;Settings &amp;gt; Redis&lt;/strong&gt; to confirm the status reads Connected. A store that measured fast in step 1 won't gain much from it, and huge autoloaded options made it slower, as step 7 shows.&lt;/p&gt;

&lt;p&gt;High-Performance Order Storage helps the same set of pages from another side. On by default for new stores since WooCommerce 8.2, it moves orders out of the WordPress posts tables into tables of their own, built for the lookups orders need, so checkout writes, the orders screen and reports get faster, while product and category pages, which don't read orders, stay exactly as they were.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 7: Keep autoloaded options and background jobs in check
&lt;/h2&gt;

&lt;p&gt;Autoloaded options are the settings WordPress reads from the wp_options table on every request, in one query, whether the page uses them or not. Plugins add to them, and many leave their rows behind after they're deleted. WordPress 6.6 stopped autoloading options larger than 150 KB unless a plugin asks for it, and &lt;strong&gt;Site Health flags a site whose autoloaded options add up to more than 800 KB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I measured what the bloat costs by adding junk options shaped like the logs and statistics caches plugins leave behind:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Autoloaded data&lt;/th&gt;
&lt;th&gt;Added to an uncached product page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;68 KB (the clean store)&lt;/td&gt;
&lt;td&gt;nothing, the baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.1 MB&lt;/td&gt;
&lt;td&gt;9 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.2 MB&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16.6 MB&lt;/td&gt;
&lt;td&gt;26 ms, plus 26 MB of memory in every PHP worker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16.6 MB with Redis on&lt;/td&gt;
&lt;td&gt;53 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Redis doubles the cost there because the object cache moves the whole block between Redis and PHP on every request. It's a slow leak, so clean it before it grows, and expect a few milliseconds back, not a new store.&lt;/p&gt;

&lt;p&gt;To see your own total and the largest rows, run this in phpMyAdmin, which most hosts include in cPanel, and replace &lt;code&gt;wp_&lt;/code&gt; with your table prefix if it differs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Total autoloaded size in KB&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;autoload_kb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_options&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;autoload&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'yes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'on'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'auto-on'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'auto'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- The 20 largest autoloaded options&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;option_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;kb&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wp_options&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;autoload&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'yes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'on'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'auto-on'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'auto'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;option_value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An option named after a plugin you removed can usually go, after a database backup. One you don't recognize belongs to something still installed, and turning its autoload off is safer than deleting it.&lt;/p&gt;

&lt;h3&gt;
  
  
  WP-Cron and Action Scheduler
&lt;/h3&gt;

&lt;p&gt;WordPress doesn't have a clock of its own. WP-Cron checks for due tasks on page loads, so on a store with a page cache it fires only when a request reaches PHP, and the background jobs WooCommerce queues in Action Scheduler wait for it: order emails, webhooks, stock syncs, subscription renewals.&lt;/p&gt;

&lt;p&gt;When it does fire, it takes a PHP worker for as long as the jobs run. The fix is a real schedule, set once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Disable the page-load trigger: add &lt;code&gt;define( 'DISABLE_WP_CRON', true );&lt;/code&gt; to wp-config.php, before the line that says to stop editing.&lt;/li&gt;
&lt;li&gt;Run cron from the server every 5 minutes: in cPanel's &lt;strong&gt;Cron Jobs&lt;/strong&gt;, add a job that calls wp-cron.php every 5 minutes at your own address, or ask your host to run it with WP-CLI.&lt;/li&gt;
&lt;li&gt;Check the queue under &lt;strong&gt;WooCommerce &amp;gt; Status &amp;gt; Scheduled Actions&lt;/strong&gt;, which lists pending, past-due and failed jobs. A long past-due list means cron isn't running often enough; thousands of failed jobs from one plugin point at the plugin.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Every 5 minutes: run the due WP-Cron events and Action Scheduler jobs&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;/5 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; wget &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-O&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"https://shop.example.com/wp-cron.php?doing_wp_cron"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Which plugins slow down WooCommerce?
&lt;/h2&gt;

&lt;p&gt;Plugins don't cost much one at a time, and they cost a lot together. I activated 16 popular plugins one by one on the test store, out of the box and not connected to their services, and measured the uncached product page with each. &lt;strong&gt;Wordfence Security added the most, 19 milliseconds&lt;/strong&gt;, followed by Jetpack, YITH WooCommerce Wishlist and WooCommerce PayPal Payments at 11 to 12 each, and six stayed under 3 milliseconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nmokv45n7zwh2d4qzob.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4nmokv45n7zwh2d4qzob.png" alt="Bar chart of the server time each plugin added to an uncached WooCommerce product page: Wordfence Security 19 ms, Jetpack 12 ms, YITH WooCommerce Wishlist 12 ms, WooCommerce PayPal Payments 11 ms, UpdraftPlus 8 ms, Yoast SEO 8 ms, Elementor 7 ms, Smush 6 ms, Site Kit by Google 4 ms, WooCommerce Stripe Gateway 4 ms; Redirection, Rank Math SEO, Mailchimp for WooCommerce, WP Mail SMTP, Contact Form 7 and MonsterInsights under 3 ms" width="800" height="770"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Server time each popular plugin added to an uncached product page, one at a time, out of the box&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then I switched 15 of them on together, the kind of stack a store collects over a few years. The product page went from 104 to 232 milliseconds, more than double.&lt;/p&gt;

&lt;p&gt;PHP loaded 4,199 files instead of 1,392 and ran 292 database queries instead of 90 for a single page, and every page the cache can't answer pays that, so the cart and the checkout feel a long plugin list long before a cached product page does.&lt;/p&gt;

&lt;p&gt;I expected the stack to cost roughly the sum of its parts. Added up one at a time, the 15 plugins came to about 91 milliseconds; together they cost 128 in one run and 139 in another, and I didn't trace where the extra time went.&lt;/p&gt;

&lt;p&gt;Plugins hook into the same WordPress events and into each other, so the only number that tells you what your stack costs is one measured with your stack. That's how I audit a store: every plugin on, then one at a time off.&lt;/p&gt;

&lt;p&gt;The browser pays a separate bill. With the 15 plugins active, the product page carried 73 script tags instead of 31, 17 stylesheets instead of 8 and 97 KB of HTML instead of 74.&lt;/p&gt;

&lt;p&gt;None of that shows up in server time, and all of it is what the Store Speed Test measures on the shopper's side, so a plugin that's cheap on the server can still be the slowest thing on the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What WP Rocket and LiteSpeed Cache fix, and what they can't
&lt;/h2&gt;

&lt;p&gt;The HTTP Archive spots caching plugins and hosts on the sites it visits every month, and the Chrome UX Report tells you how those sites did for real visitors, so together they show how often each group gives phones a good server response and passes Loading.&lt;/p&gt;

&lt;p&gt;Don't skip the caveat: these rows count every site where the technology was detected, any platform, not only stores, because the data can't be narrowed to sites that run WooCommerce and the plugin together.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Sites&lt;/th&gt;
&lt;th&gt;Pass all three&lt;/th&gt;
&lt;th&gt;Good server response&lt;/th&gt;
&lt;th&gt;Good Loading&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WooCommerce, all sites&lt;/td&gt;
&lt;td&gt;557,369&lt;/td&gt;
&lt;td&gt;39.5%&lt;/td&gt;
&lt;td&gt;12.7%&lt;/td&gt;
&lt;td&gt;43.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WordPress, all sites&lt;/td&gt;
&lt;td&gt;2,752,367&lt;/td&gt;
&lt;td&gt;48.7%&lt;/td&gt;
&lt;td&gt;23.5%&lt;/td&gt;
&lt;td&gt;55.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WP Rocket&lt;/td&gt;
&lt;td&gt;272,660&lt;/td&gt;
&lt;td&gt;51.3%&lt;/td&gt;
&lt;td&gt;30.0%&lt;/td&gt;
&lt;td&gt;61.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteSpeed Cache&lt;/td&gt;
&lt;td&gt;391,972&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;td&gt;29.8%&lt;/td&gt;
&lt;td&gt;59.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W3 Total Cache&lt;/td&gt;
&lt;td&gt;81,968&lt;/td&gt;
&lt;td&gt;44.5%&lt;/td&gt;
&lt;td&gt;21.7%&lt;/td&gt;
&lt;td&gt;51.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WP Fastest Cache&lt;/td&gt;
&lt;td&gt;41,670&lt;/td&gt;
&lt;td&gt;55.8%&lt;/td&gt;
&lt;td&gt;42.2%&lt;/td&gt;
&lt;td&gt;64.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NitroPack&lt;/td&gt;
&lt;td&gt;11,320&lt;/td&gt;
&lt;td&gt;56.5%&lt;/td&gt;
&lt;td&gt;23.5%&lt;/td&gt;
&lt;td&gt;69.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FlyingPress&lt;/td&gt;
&lt;td&gt;4,585&lt;/td&gt;
&lt;td&gt;56.6%&lt;/td&gt;
&lt;td&gt;37.7%&lt;/td&gt;
&lt;td&gt;70.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perfmatters&lt;/td&gt;
&lt;td&gt;17,195&lt;/td&gt;
&lt;td&gt;53.7%&lt;/td&gt;
&lt;td&gt;29.8%&lt;/td&gt;
&lt;td&gt;65.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autoptimize&lt;/td&gt;
&lt;td&gt;78,265&lt;/td&gt;
&lt;td&gt;46.2%&lt;/td&gt;
&lt;td&gt;19.4%&lt;/td&gt;
&lt;td&gt;53.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WP-Optimize&lt;/td&gt;
&lt;td&gt;48,087&lt;/td&gt;
&lt;td&gt;49.4%&lt;/td&gt;
&lt;td&gt;27.2%&lt;/td&gt;
&lt;td&gt;56.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Share of sites with each technology detected, mobile, from real Chrome visitors, August 2026. Server response is good at 800 milliseconds or less, Loading at 2.5 seconds or less.&lt;/p&gt;

&lt;p&gt;WP Rocket and LiteSpeed Cache sites beat WordPress as a whole on every column, W3 Total Cache and Autoptimize sites trail it, and still only about 3 in 10 WP Rocket and LiteSpeed Cache sites give phones a good server response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cache plugin can only make cached pages fast&lt;/strong&gt;: it can't shorten the distance to a server on another continent, answer the cart and checkout, catch ad clicks it wasn't told to ignore, or add CPU to a host that has none to spare.&lt;/p&gt;

&lt;p&gt;NitroPack shows the other side of the trade, with one of the best Loading rates in the table, 69.3%, from heavy image and script work in the browser, and server response at 23.5%, level with WordPress overall.&lt;/p&gt;

&lt;p&gt;The hosts' rows count every site on the host, any software, any country and any plan, so they describe the customers as much as the servers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Host or server&lt;/th&gt;
&lt;th&gt;Sites&lt;/th&gt;
&lt;th&gt;Pass all three&lt;/th&gt;
&lt;th&gt;Good server response&lt;/th&gt;
&lt;th&gt;Good Loading&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pantheon&lt;/td&gt;
&lt;td&gt;14,376&lt;/td&gt;
&lt;td&gt;71.2%&lt;/td&gt;
&lt;td&gt;48.6%&lt;/td&gt;
&lt;td&gt;81.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WP Engine&lt;/td&gt;
&lt;td&gt;83,985&lt;/td&gt;
&lt;td&gt;65.0%&lt;/td&gt;
&lt;td&gt;31.2%&lt;/td&gt;
&lt;td&gt;75.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kinsta&lt;/td&gt;
&lt;td&gt;31,012&lt;/td&gt;
&lt;td&gt;58.6%&lt;/td&gt;
&lt;td&gt;21.3%&lt;/td&gt;
&lt;td&gt;66.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SiteGround&lt;/td&gt;
&lt;td&gt;112,334&lt;/td&gt;
&lt;td&gt;52.4%&lt;/td&gt;
&lt;td&gt;21.6%&lt;/td&gt;
&lt;td&gt;59.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hostinger&lt;/td&gt;
&lt;td&gt;349,478&lt;/td&gt;
&lt;td&gt;46.6%&lt;/td&gt;
&lt;td&gt;30.4%&lt;/td&gt;
&lt;td&gt;56.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bluehost&lt;/td&gt;
&lt;td&gt;36,414&lt;/td&gt;
&lt;td&gt;43.1%&lt;/td&gt;
&lt;td&gt;21.6%&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Varnish (any host)&lt;/td&gt;
&lt;td&gt;152,229&lt;/td&gt;
&lt;td&gt;60.8%&lt;/td&gt;
&lt;td&gt;59.7%&lt;/td&gt;
&lt;td&gt;80.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudflare (any host)&lt;/td&gt;
&lt;td&gt;3,979,838&lt;/td&gt;
&lt;td&gt;50.7%&lt;/td&gt;
&lt;td&gt;47.3%&lt;/td&gt;
&lt;td&gt;64.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LiteSpeed web server&lt;/td&gt;
&lt;td&gt;558,939&lt;/td&gt;
&lt;td&gt;50.0%&lt;/td&gt;
&lt;td&gt;31.2%&lt;/td&gt;
&lt;td&gt;58.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nginx&lt;/td&gt;
&lt;td&gt;2,094,351&lt;/td&gt;
&lt;td&gt;56.0%&lt;/td&gt;
&lt;td&gt;42.2%&lt;/td&gt;
&lt;td&gt;68.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same source and date. Varnish and Cloudflare rows count every site that sends their headers, whoever the host is.&lt;/p&gt;

&lt;p&gt;No host on the list gives even half of its sites a good server response; Pantheon comes closest at 48.6%. Sites that send Varnish headers, a cache that sits in front of the web server, beat every host at 59.7%, which matches the benchmark: the layer that answers before PHP decides the number.&lt;/p&gt;

&lt;p&gt;Kinsta, SiteGround and Bluehost sit near 21%, worse than WordPress as a whole. The host sets the ceiling, and the cache setup and the plugins decide how close you'll get to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the benchmark was run
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hardware: my workstation, an AMD Ryzen 9 3900X, with the PHP container limited to 1, 2 or 4 cores. One core with two PHP workers stands in for a budget shared plan; four cores with eight workers for a small dedicated server.&lt;/li&gt;
&lt;li&gt;Software: WordPress 7.1, WooCommerce 11.1.2 with High-Performance Order Storage on, Storefront 4.6.2, PHP 8.3 in the official WordPress PHP-FPM image (plus the PhpRedis extension), MariaDB 11.4 with a 1 GB buffer pool, Redis 7 and nginx 1.27, all in Docker.&lt;/li&gt;
&lt;li&gt;Catalog: 600 products in 12 categories, every third one a variable product with 6 variations, long descriptions, a featured image and three gallery images, built by a seeded generator so every run builds the same store.&lt;/li&gt;
&lt;li&gt;Measurement: time to the first byte, taken by a client inside the same Docker network, so the network adds almost nothing. Sequential tests are the median of 40 requests after 5 warm-up requests; load tests keep 20 shoppers asking without pause for 20 seconds.&lt;/li&gt;
&lt;li&gt;Page cache: Cache Enabler 1.8 for the plugin, nginx fastcgi_cache for the server, with the cart, checkout, account pages, wc-ajax requests and WooCommerce cookies excluded.&lt;/li&gt;
&lt;li&gt;Plugins: each one activated alone on two cores with OPcache, measured, and deactivated; then 15 together, with Rank Math left out because it does the same job as Yoast SEO. Out of the box: not connected to their services and not configured.&lt;/li&gt;
&lt;li&gt;The two stores: one product page each, measured from my connection in Poland on October 4, 2026, three requests per test, with the connection setup subtracted; their wc-ajax requests recorded in a fresh Chrome profile with nothing in the cart; the screenshots from my own Chrome.&lt;/li&gt;
&lt;li&gt;Cart fragments snippet: tested in Chrome on the test store with and without it: a first visit, Add to cart, the same page again in the same tab, then a new tab.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A lab box is quieter than a shared host, where other customers' sites use the same CPU, and its database sits next to PHP with every row in memory, so you'll find real servers slower than these numbers. The ratios between setups are the part that travels: what OPcache, a page cache, an extra core or a plugin does to the same page.&lt;/p&gt;

&lt;p&gt;And a faster theme won't fix the rest on its own. Storefront is one of the lightest WooCommerce themes, and the test store still needed 473 milliseconds per page without OPcache and 100 with it, before a single extra plugin was added. A theme decides how much work each page takes; the server and the cache decide how often that work happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;WooCommerce developer blog: &lt;a href="https://developer.woocommerce.com/2023/06/16/best-practices-for-the-use-of-the-cart-fragments-api/" rel="noopener noreferrer"&gt;Best practices for the use of the cart fragments API&lt;/a&gt;, June 16, 2023.&lt;/li&gt;
&lt;li&gt;WordPress core: &lt;a href="https://make.wordpress.org/core/2024/06/18/options-api-disabling-autoload-for-large-options/" rel="noopener noreferrer"&gt;Options API: disabling autoload for large options&lt;/a&gt; (WordPress 6.6), and &lt;a href="https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/" rel="noopener noreferrer"&gt;Hooking WP-Cron into the system task scheduler&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;WP Rocket: &lt;a href="https://docs.wp-rocket.me/article/971-caching-query-strings" rel="noopener noreferrer"&gt;Caching query strings&lt;/a&gt;. LiteSpeed: &lt;a href="https://docs.litespeedtech.com/lscache/lscwp/cache/" rel="noopener noreferrer"&gt;LSCache for WordPress, Cache settings&lt;/a&gt;. Cloudflare: &lt;a href="https://developers.cloudflare.com/cache/how-to/cache-rules/settings/" rel="noopener noreferrer"&gt;Cache Rules settings&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;PHP: &lt;a href="https://www.php.net/supported-versions.php" rel="noopener noreferrer"&gt;Supported versions&lt;/a&gt;. Google, web.dev: &lt;a href="https://web.dev/articles/ttfb" rel="noopener noreferrer"&gt;Time to First Byte&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Field data: &lt;a href="https://httparchive.org/reports/techreport/tech" rel="noopener noreferrer"&gt;HTTP Archive Technology Report&lt;/a&gt;, August 2026, and CartHacker's open dataset behind &lt;a href="https://carthacker.com/trends/ecommerce-speed-statistics/" rel="noopener noreferrer"&gt;ecommerce speed by platform&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>woocommerce</category>
      <category>wordpress</category>
      <category>performance</category>
      <category>php</category>
    </item>
  </channel>
</rss>
