How to Optimize WooCommerce Performance

WooCommerce is more flexible than any other eCommerce platform because of its repository of free and premium extensions and thousands of compatible plugins. But, this flexibility is a double-edged sword.

Many plugin authors don’t actually test their functionality on large stores or with large volumes of users or orders. As a result, there’s a myth out there that WooCommerce doesn’t scale.

Fortunately, you can troubleshoot and fix WooCommerce performance issues.

Today, we’ll share more detail on why WooCommerce stores can have performance issues, how far you can scale WooCommerce as well as how to troubleshoot and fix performance problems.

Table of contents:

Why WooCommerce Can Be Slow

Some developers may say WooCommerce slows down and will not scale past 5,000 or 10,000 products depending on your hosting. The main reason for this is poor code.

It’s enough to have just one poorly coded plugin or copy and pasted snippet enabled in your theme. And it can already cause WordPress to access every product in your store in order to produce your product archive page.

You won’t notice this problem when you only have a few hundred or a few thousand products. But when you grow your store, you’ll soon find that your CPU is maxed out and pages are taking upwards of five seconds to generate.

Less common pitfalls include badly configured product variations and product attributes which can cause the WooCommerce code to access and process far too much data on your product archive pages. This leads to really slow Time to First Byte (TTFB), which is a measurement of how long a page takes to generate.

This frequently leads to excessive RAM consumption as well as your CPUs maxing out. Ultimately, you’ll be forced to keep spending more and more money on more expensive hosting to get the speed you need for your established store.

Under the hood, there are two primary problems occurring. The first is that a WooCommerce plugin, theme, or your custom code is fetching all WooCommerce products, then looping through that data using PHP. These loops use up RAM and CPU.

A related issue is that some code forces what’s known as table scans to occur even if the PHP code is well written. This means even if the code is only presenting 20 products to users, the MySQL code still fetches every single product from your database.

Not only does this use up RAM and CPU on your server, but it also flushes some of the MySQL caches making future queries perform more slowly.

How Far WooCommerce Can Scale

Scaling refers to how many products your store can handle and how many simultaneous users and orders it can handle without slowing down.

MySQL, the database used by WordPress and WooCommerce, is a relational database. It includes indexes that can be used to quickly access the data the code requires without accessing every product, user, or order in your database.

MySQL uses b-trees in their indexes to make access to data really fast.

Let’s look at an example:

If you have one million items in a b-tree, it will only take 20 reads to access the data rather than one million reads. The base of these b-trees actually gets larger the more data you have meaning there is no real limit on the size your WooCommerce store can become.

An example of this is the Found Thru demo store which has about 800,000 products and it’s running on a $20 per month server. Every page, search, or filter returns uncached results in under two seconds.

This example should help demonstrate that there’s really no limit when it comes to the size of the WooCommerce store you want to achieve.

Why Hosting Matters

If your WooCommerce store is on shared hosting, you should consider getting a self-hosted server or managed hosting provider. This helps ensure your hosting has ample resources to accommodate your store as you scale it.

Some key things you need for fast WooCommerce performance include:

  • SSD – Solid-state drives are far faster than hard disks drives.
  • Redis object cache – An object cache can share fragments of cache between multiple pages, even with logged-in users, making it essential for eliminating load from your server.
  • PHP – Have at least PHP 7.4 running.
  • MySQL – You should have version 8 or above of MySQL. Or, you could opt for using MariaDB or Percona.
  • HTTP/2 or HTTP/3 – Compared to HTTP/1, these allow browsers to download far more files simultaneously from your website. In turn, this eliminates the need to concatenate files, which is recommended by many performance guides but can be tricky to pull off and might even break your site. So, you should migrate to a hosting provider that offers at least HTTP/2.
  • Page Caching – Most good hosts will provide some form of page caching through Litespeed, NGINX, or similar. You’ll also need a way of clearing this page cache. Having page caching at your server-level normally eliminates the need for a caching plugin.

Other than that, it’s helpful to have a good interface to manage your site, and a Content Delivery Network (CDN) to deliver your static files, which helps reduce server requests.

Your host’s interface should let you create server backups as well as development clones, and (ideally) migrate easily from staging to live.

How to Troubleshoot WooCommerce Performance

Troubleshooting WooCommerce performance is probably the most complicated part of optimizing your WooCommerce store. It can be tricky to know where the problem originated.

Even worse, the nature of bottlenecks in performance and scaling is such that if you have three or four bottlenecks, removing just one of those bottlenecks will not help you much. You need to fix all of the bottlenecks to get the performance and scale that WooCommerce is capable of achieving.

One of the best tools to help find these problems is the plugin called Query Monitor.

pressjitsu optimize WooCommerce Query Monitor plugin

Let’s take a look at the best ways to troubleshoot your WooCommerce website.

1. Using Query Monitor to Troubleshoot and Find Slow Queries

The first thing you should do is install the Query Monitor plugin, then load your slow archive pages.

Within Query Monitor, order the queries by time taken. If you have any queries taking more than 0.1 second (yes, just that low!) then this is a problem.

Often, on your archive pages you’ll see single queries taking 5 seconds or more if your store is large enough. This is a result of the query not using an index.

Below, you can see two console screenshots of a query that we have previously optimized. In this example, the wp_posts table has 5 million posts and the slow query was taking about 5 minutes to complete.

pressjitsu optimize WooCommerce code index

As you can see, before optimization this single SQL query from the Yoast SEO plugin took over 4 minutes to generate the XML sitemap index.

In the case of this specific query, checking the post_date was redundant. And because there is an index on the other columns, removing that line produces the same functionality in under one second:

pressjitsu optimize WooCommerce optimized query

If you discover slow queries, you should ask a developer to fix them. Normally this is done by rewriting the query (as above), but sometimes you can add an additional index to a table to serve that query and speed it up enormously.

Using the example above, firstly we need to eliminate the != clause from the query – indexes cannot use columns in the index after a not-equals clause has been used. With the post_date != ‘0000-00-00 00:00:00’ the query would look like this:

SELECT wp_posts.ID
FROM wp_posts
WHERE wp_posts.post_status = 'publish'
AND wp_posts.post_type = 'product'
AND wp_posts.post_password = ''
ORDER BY wp_posts.post_modified ASC LIMIT 10 OFFSET 483300;

You can see the query is doing the filtering based on the following columns:

post_status, post_type, post_password

And then it sorts the results by the post_modified column in ascending (ASC) order. So, if you wished to create an index to boost this query rather than modifying the query to use an existing index you could create an index like this:

CREATE INDEX ixSpeedUpYoast on wp_posts(post_status, post_type, post_password, post_modified);

Note: You do not need to add the ID column into your index (referenced in the select statement) because it is the primary key, and as such it exists in every index naturally.

2. Using Query Monitor to Find Bad Coding Patterns

Within Query Monitor, access the tab called Queries by Caller/Component and check if there are plugins with hundreds of SQL queries or more. If you find that there are, it means that the plugin or a part of its functionality is causing the issues.

The Queries by Caller view shows you which PHP functions are generating these queries. There may be commonality across different plugins in this case.

The Queries by Component view shows you which WordPress plugins are generating these queries.

Next, order the queries on the main tab by row count in Query Monitor. Any plugins that are returning thousands of rows are also likely creating a bottleneck by consuming RAM.

If you see either of these symptoms, you should look at the plugin’s code. You’ll likely see bad coding patterns that are discussed later in this article in the Optimizing Themes, Plugins, and Custom Code section.

3. Switching to a Different Theme

On your development server, try switching to a different theme and test your performance again. Check with Query Monitor to see if the performance of your site improves. If it does, then you know there’s some code somewhere in your theme that’s performing either a loop or table scan against the database.

You can use the same Query Monitor functionality to compare performance before and after switching themes. This way you can discover if there is any functionality part of your theme that’s causing an issue.

4. Disabling Plugins

While you’re still on your development server, you can do some further troubleshooting by disabling multiple plugins at once until you reach fast performance on your archive pages.

Then, reactivate the plugins one-by-one until your speed tanks. The last plugin you reactivated is the culprit.

5. Debugging When You’re Still Having Issues

If you’re still struggling with discovering the performance issues, you can use a tool called Xdebug which produces a chart of where all your RAM and CPU consumption is being used.

This will ultimately tell you which plugin is the culprit.

Once you know which plugin is causing a bottleneck, you can fix it by either changing options inside the plugin, or by replacing the plugin entirely with a better one. Sometimes, only one feature of a plugin is badly coded, and disabling it can solve the problem.

You could also alter the code of the plugin yourself or you can hire developers to do it for you.

How to Optimize WooCommerce Performance

Once the troubleshooting is done and you have found the problem, you can fix the performance bottleneck. Below are the most common issues and fixes available to help you scale.

1. Optimizing WooCommerce Variations

Before taking a look at how to optimize WooCommerce variations, it’s important to understand how variations work in WooCommerce.

The way variations are coded inside WooCommerce can cause issues depending on how you use them. If you have a single product with three selectable options and each of those has three choices, then under the hood, WooCommerce will create 28 products. The parent product plus 3 x 3 x 3 child variations.

Having 28 variations is not so problematic, but if instead you have five selectable options and each of those has 10 choices, then WooCommerce will create 100,001 products.

Now, you’re in problem territory.

Redis object caching really helps with product variations but if you have configured your products so there are more than 500 variations per product, then you should think about whether each of those variations needs to exist.

To know if a variation is needed, ask yourself if you need a separate image for each variation and if each variation has a unique price. In many cases, you could use product options instead of product variations.

Product options include cases where businesses have rules like:

  • Medium size + $10
  • Large size + $15

These rules for these different sizes mean that WooCommerce doesn’t need to create child product variations for this product attribute. Instead, if a user chooses the medium size, then a simple rule is applied to increase the price.

In the example above, if it’s possible to change three out of the five variation options into product rules, then instead of 100,001 product variations there would only be 101. That’s far more manageable for PHP to process.

2. Optimizing WooCommerce Attributes

Attributes are the options that users can select on your product archive pages to filter the results.

For example, color, material, and flavor can all be attributes.

There are some plugins out there that will attempt to auto-create attributes for you. It’s highly recommended that you avoid these and create attributes yourself since these auto attributes can end up in the thousands which will kill the scalability and performance of your store.

Typically, a well-designed store will have somewhere in the region of three to 10 attributes per category and probably no more than 30-50 attributes store-wide.

With your attributes properly configured, the next issue to contend with is the filters that use these attributes.

Unfortunately, the default WooCommerce filters are not scalable. The category filter is well designed and well coded, but the price filter and attribute filter will fetch all of your products using MySQL, then filter out the results using PHP to produce the first page of 20 results for your users.

This totally ruins the advantages of using a relational database that can perform filters incredibly quickly if the queries use indexes.

3. Optimizing WooCommerce Filters and Shortcodes

If you have filters on your store, there are two plugins in particular that perform and scale well.

The first of these is ElasticPress. It uses Elasticsearch under the hood so you’ll either need to be able to install Elasticsearch on your server or commit to paying for their hosted option.

Elasticsearch is a separate database from your MySQL database so the ElasticPress plugin copies your data over to Elasticsearch. Then, your product archive pages are set to use Elasticsearch rather than MySQL to produce the filtered results. The ElasticPress plugin handles all of this for you.

The primary issue with ElasticPress is pricing. If you have fewer than 20,000 products it’s $79 per month, or $299 for 250,000 products. If you have more products than that, you’ll be charged a minimum of $699 per month.

pressjitsu optimize WooCommerce performance Elasticpress

On the other hand, Faster Woo Widgets doesn’t cost you an extra monthly fee based on how many products you have in your store and is priced like a normal WordPress plugin.

It makes use of your existing MySQL database and rewrites the SQL queries to use indexes and fragment caching.

This means that the first time your /shop/ page loads on a million-product store, it may take 10 seconds to generate that page including counts of products in each category and attribute. But, this top-level query is then cached and reused for subsequent filters so future filters are far faster.

This top-level shop query is also reused across all of your users.

There are also some built-in shortcodes with WooCommerce you should avoid due to how their MySQL queries are coded. This includes “products on sale” and “best selling products.”

Faster Woo Widgets includes a products-on-sale plugin, but it doesn’t yet include a solution for best-selling products.

Currently, the best approach is to use “featured products” instead of the best-selling products shortcode. Or, create static content. Most implementations use the best-selling products shortcode on the home page so creating static content here should be simple enough for most users.

4. Optimizing Themes and Plugins

One of the biggest scalability problems in WooCommerce themes is a little feature that changes the “On Sale” flag to show the percentage discount. This feature is not a problem if you’re not using product variations.

But, if you are using them, then you should be aware that with most themes this feature performs a PHP loop and fetches every product variant into RAM to produce the percentage discount figure. It’s a perfect example of code that works but slows down as your WooCommerce store grows in size.

The easiest solution would be to disable this problematic piece of functionality, but you can instead alter this code so it performs well – if you feel brave enough.

Here is a copy of this code that is distributed in many themes:

add_action( 'woocommerce_before_shop_loop_item_title', 'bbloomer_show_sale_percentage_loop', 25 );

function bbloomer_show_sale_percentage_loop() {
   global $product;
   if ( ! $product->is_on_sale() ) return;
   if ( $product->is_type( 'simple' ) ) {
      $max_percentage = ( ( $product->get_regular_price() - $product->get_sale_price() ) / $product->get_regular_price() ) * 100;
   } elseif ( $product->is_type( 'variable' ) ) {
      $max_percentage = 0;
      foreach ( $product->get_children() as $child_id ) {
         $variation = wc_get_product( $child_id );
         $price = $variation->get_regular_price();
         $sale = $variation->get_sale_price();
         if ( $price != 0 && ! empty( $sale ) ) $percentage = ( $price - $sale ) / $price * 100;
         if ( $percentage > $max_percentage ) {
            $max_percentage = $percentage;
         }
      }
}
if ( $max_percentage > 0 ) echo "<div class='sale-perc'>-" . round($max_percentage) . "%</div>"; 
}

The problem pattern starts with this line:

foreach ( $product->get_children() as $child_id ) {...}

That loop fetches all the variation children of each product on your page into RAM using PHP. When you have hundreds or thousands of variations this uses up a lot of RAM and CPU, slowing down your site and reducing how many visitors can use the site at one time.

Instead of fetching each entire child product, there’s actually a function we can call to get all the variation prices. And that is enough to calculate the maximum percentage discount available for the product.

If you change the loop as follows, you’ll see a massive speed boost on larger stores with lots of variations:

$prices = $product->get_variation_prices( true ); // this is the fastest way to get the prices for all variants - returns multidimensional array from object cache
foreach($prices['regular_price'] as $pid => $regular_price) {
   if ($regular_price == 0) continue; // if regular price is 0, skip this product
   if ($regular_price == $prices['sale_price'][$pid]) continue; // if sale price = regular price, skip this product
   $percentage = ( $regular_price - $prices['sale_price'][$pid] ) / $regular_price * 100;
   if ( $percentage > $max_percentage ) {
      $max_percentage = $percentage;
   }
}

Here’s the full code with this change in place:

<?php
/**
* @snippet Display Discount Percentage @ Loop Pages - WooCommerce
* @how-to Get CustomizeWoo.com FREE
* @sourcecode https://businessbloomer.com/?p=21997
* @author Rodolfo Melogli
* @compatible WooCommerce 3.5.4
* @donate $9 https://businessbloomer.com/bloomer-armada/
*/

add_action( 'woocommerce_before_shop_loop_item_title', 'bbloomer_show_sale_percentage_loop', 25 );

function bbloomer_show_sale_percentage_loop() {
   global $product;
   if ( ! $product->is_on_sale() ) return;
   if ( $product->is_type( 'simple' ) ) {
      $max_percentage = ( ( $product->get_regular_price() - $product->get_sale_price() ) / $product->get_regular_price() ) * 100;
   } elseif ( $product->is_type( 'variable' ) ) {
      $max_percentage = 0;
      // Changed by Dave Hilditch @ WP Intense to be far faster on archive pages with many child variations
      $prices = $product->get_variation_prices( true ); // this is the fastest way to get the prices for all variants - returns multidimensional array from object cache
      foreach($prices['regular_price'] as $pid => $regular_price) {
         if ($regular_price == 0) continue; // if regular price is 0, skip this product
         if ($regular_price == $prices['sale_price'][$pid]) continue; // if sale price = regular price, skip this product
         $percentage = ( $regular_price - $prices['sale_price'][$pid] ) / $regular_price * 100;
         if ( $percentage > $max_percentage ) {
            $max_percentage = $percentage;
         }
      }
      /*
      foreach ( $product->get_children() as $child_id ) {
         $variation = wc_get_product( $child_id );
         $price = $variation->get_regular_price();
         $sale = $variation->get_sale_price();
         if ( $price != 0 && ! empty( $sale ) ) $percentage = ( $price - $sale ) / $price * 100;
         if ( $percentage > $max_percentage ) {
            $max_percentage = $percentage;
         }
      }
      */
   }
   if ( $max_percentage > 0 ) echo "<div class='sale-perc'>-" . round($max_percentage) . "%</div>"; 
}

Another common problem area in themes is built-in filtering systems. If your theme has such a system in place, you can be almost certain that it follows the previously referenced bad design pattern which loops through all of your products. So, you should either use ElasticPress or Faster Woo Widgets instead of the built-in filtering feature.

In addition to problematic themes, many plugins also include poorly designed code which will fetch all of your products and loop through them using PHP. To spot these, you can use Query Monitor to identify the rough location of the code causing 100s or 1000s of queries and then scan the code looking for this pattern.

Query Monitor Queries by Caller will show you woocommerce_before_shop_loop_item_title when really it would be more helpful to see bbloomer_show_sale_percentage_loop. It struggles to identify the true source of code when the queries exist inside filters and actions so it can’t tell you precisely where the problem queries are coming from.

In these cases, you can use Xdebug to analyze the call stacks and dive deep inside the code to find your bottlenecks.

pressjitsu optimize WooCommerce xdebug

Using Xdebug is fairly straightforward. Firstly, install Xdebug onto your development server and configure the profiler query string by adding these two lines to your php.ini file:

xdebug.profiler_enable_trigger = 1
xdebug.profiler_enable_trigger_value = runprofile
xdebug.profiler_output_dir = /var/log/xprofiler/
xdebug.profiler_output_name = cachegrind.out.%t.%p

These lines are in addition to the lines added to your php.ini file when you follow the Xdebug installation guide.

Next, restart your PHP service and load your problem page with the following text appended to the URL:

?XDEBUG_PROFILE=runprofile

This indicates to Xdebug that it should collect the profiling data and save it to the folder specified in your xdebug.profiler_output_dir directive.

Then, download the generated cachegrind files from your profiler_output_dir location and open them using QCacheGrind (ensure you get the 64-bit version as it can handle any size of cachegrind files). Depending on the size of your cachegrind files, it may take a few minutes to open them. Once the files are open, you can browse around inside the actual PHP functions and visually see which parts of PHP code are consuming CPU and/or RAM.

5. Optimizing Your Custom Code

If you have added custom code to your child theme, the primary thing you should avoid is fetching products inside a loop. The correct pattern is to fetch the restricted result set you need, then loop through those rather than creating a loop of parent products and fetching all the children inside that loop. An example of this is above with the rewrite of the bbloomer_show_sale_percentage_loop function – it uses a function to grab ALL variation prices outside of the loop and then loops through that to produce the resulting percentage sale discount.

You can spot if your custom code is problematic by re-enabling your parent theme and checking the query count using the Query Monitor plugin.

6. Further Optimizations

There are some other more obvious improvements you can make to speed up your WooCommerce store. Many guides will talk about your Google Page Speed score, your Pingdom score, or your GTMetrix score but those scores don’t represent real life. They don’t take into account, for example, how long your site actually takes to produce the page which is fundamentally the most important aspect of performance for your users.

pressjitsu optimize WooCommerce Google pagespeed
Google corporate scores 25 out of 100 in their own page speed test. Try loading https://about.google/ and see if you think it’s slow…

Having said that, there are further boosts you can gain by optimizing your Google Page Speed scores

So, you should run your site URLs through their page analyzers and follow their guides. Mostly it involves using a CDN and reducing your image sizes.

If you see recommendations to concatenate files, you can safely ignore them – if you are using HTTP/2 or HTTP/3. The original recommendation from these page analyzers to concatenate files happened because HTTP/1 only allowed three files to be downloaded from one server simultaneously, increasing page load times.

You may also wish to experiment with caching plugins.

If you already have page caching through your web host then you most probably won’t need a caching plugin, other than the Redis object cache.

If your host offers page caching, or if you are using a CDN that provides full-page caching then you may not see the benefit from using a WordPress page caching plugin. In fact, you’re adding load to your server, instructing it to save every page to disk (or to Redis depending on how you configured your page caching plugin).

Often, caching plugins also offer minification and concatenation. You should experiment with these, too, to see if they boost your speed. However, if your host provides HTTP/2 then you’ll probably see your site slow down, rather than speed up, as you’re adding this extra work to your server.

Summary

Despite what some may say, WooCommerce can scale. You can create stores containing millions of products on very affordable hosting provided you’re careful about which functionality you enable and how you enable it.

Make sure you choose a great hosting service to give yourself a good base, then follow the guidelines above to eliminate performance bottlenecks.

Were you able to successfully scale WooCommerce and fix your online store’s performance problems? Are you still experiencing issues? Share your experience in the comments below.

Перевод: https://pressjitsu.com/blog/optimize-woocommerce-performance/

 

Геннадий Ковшенин

Гениальный программист (таких по пальцам пересчитать). Жил на Мальте, потом в Магнитогорске, потом в Санкт-Петербурге. Один из основатель всего российского WP сообщества, вечный спонсор всех WordCamp'ов. Создатель супер-быстрого и супер-защищенного WordPress хостинга PressJitsu и (возможно) настоящий создатель Биткоина. Может за 15 минут решить проблему, над которой полгода бился весь ваш IT-отдел. Вряд ли захочет, потому что все еще находится в поисках себя. Может стать кем угодно, по настроению. Как-то стал сверхмарафонцем и пробежал 100км Ural Ultra Trail. Очень любит WordPress, зеленый чай и аттракционы. Тролль 543 левела. До сих пор не сделал сам ни одного сайта на WordPress.

Добавить комментарий

%d такие блоггеры, как: