Advanced WordPress optimization. Part 1: main points

In the article about «WordPress with smart Varnish caching» I talked about the acceleration of the website using Varnish reverse proxy which generates HTML pages and returns them in the form of static. But it also happens that your website receives a high load from authorized users or there are some parts that are not acceptable to cache.

For example, it could be a store on Woocommerce with more than 10 000 products with lots of plugins, multi-level discount programs e.t.c.

In a situation with a dynamic load, more advanced and, to some extent, individual optimization techniques will have to be applied. That’s why I’m starting a series of articles where I will try to describe all the possible steps that I had to do in mine practice. So you could use them adapted to your situation.

Artyom Zaytsev

Evilmarketer

Disable WP_Cron

WP_Cron on sites hung with plugins often produce a huge load. The built-in task scheduler on WordPress works in such a way that every time you load pages, you pass requests to wp-cron.php. Such requests check if there are any tasks to be executed at this time or not. Obviously, such a mechanism produce an additional unnecessary load, which could and should be removed.
To remove the extra load and put wp_cron running at the same time, we’ll disable it and will be requesting the wp-cron.php via native server cron.
How to disable
To disable WP-cron you need to add the following line in wp-config.php:
define('DISABLE_WP_CRON', true);
In the D2C panel, in the service that runs our application, we will create a cron task for making requests to wp-cron.php every 5 minutes:
#using cURL. It's included out of the box
*/5 * * * *
cURL https://yoursite.com/wp-cron.php?doing_wp_cron

In the panel it looks like:

#Using wget, if installed.
*/5 * * * *
wget -O - -q -t 1 https://yoursite.com/wp-cron.php?doing_wp_cron

The -O option outputs to the console instead of saving to a file; -q does it silently, without displaying it; -t 1 specifies that only one connection attempt should be made..

In the panel it looks like:

Enable object cache

Object cache allows to serve executed data of any type, and get it from RAM when you request it. Object cache allows to serve executed data of any type, and get it from RAM when you request it. If we add it to our stack, we can get most of the data by excluding MySQL. It will improve performance.

In WordPress, you can use 2 solutions: Memcached and Redis. To enable Memcached, you will need a plugin. Usually, I use Memcached Redux.

How to enable

First, you need to add a service in the D2C panel and copy the alias ant the port. We’ll need them later.

To set up WordPress to work with Memcached, you need to put the object-cache.php from the archive in the wp-content folder and specify in wp-config.php an array with aliases of cache servers. The plugin itself is not necessary to put in the plugins folder.
In my case, there is a single service with an alias dokmemcached, so I’ll specify it in the config:
$memcached_servers = array('dokmemcached:11211');
In addition to aliases, you may specify a unique WP_CACHE_KEY_SALT. This constant is needed for different sites to have access to the same caching server without conflict. You can specify anything you want, but I prefer to specify the name of the site:
define('WP_CACHE_KEY_SALT', 'dokmarket');

If you did everything correctly, the site will work. If you made mistakes, you will see a 500 error.

Efficiency

Using Memcached reduces lots of database requests. The object cache data itself is stored in memory, which gives a strong advantage in the speed of access to it. The example with the same site:

The product page without the object cache 
SQL: 209, 1.000 sec.

The product page with the object cache
SQL: 45, 0.000 sec.

Outputting similar goods, without the use of woocommerce_related_products()

The solution is for owners of big Woocommerce stores.

Woocommerce has its own woocommerce_related_products() function, which is used to implement a block of similar products. Its problem is that requests for outputting related products Woocommerce do in an extremely costly way: it pulls a bunch of metadata, looking for all products and then displays them randomly, using ‘orderby’ => ‘rand’ in wp_query.

‘orderby’ => ‘rand’ should be rewritten ruthlessly and everywhere. This method is probably one of the most insatiable in WP. In my practice, there was a case when such a method generated as much as 1000 queries to the database.

There are 2 ways to solve the problem: put an additional layer with Elasticsearch to the stack and send requests to it or change the output logic to a less expensive method. In the paragraph, I will tell about the second option.

Before implementing the idea, it is necessary to determine the logic. If you look from the user’s point, it is not particularly useful to see random products among similar ones. In this case, it makes sense to display products by popularity (number of sales), rating or cost (from cheap to expensive). If the project is new and does not have enough sales statistics or a large number of reviews, I use the price filter.

Implementation
To start, we’ll create an empty file in the folder of the active theme, let’s name it, for example, content-product-related.php.

Above you can see the folder “woocommerce”, it contains store templates. If your current theme does not have one, you can create the folder and copy files from the Woocommerce plugin templates folder. So you can manage your store templates directly from the theme.

So, let’s start our work from the content-product-related.php. First of all, we should deny direct access to the file.
if ( ! defined( 'ABSPATH' ) ) exit; 
Next, we need to call the global variable $products and make sure that we are on the product page:
global $product;
if ( ! $product || ! $product->is_visible() ) {
	return;
}

We will also need some data about the product. It will be the product ID, and information about the categories related to the product.

$post_id = get_the_ID();
$term_list_product_cat = wp_get_post_terms( $post_id, 'product_cat', array("fields" => "all") );//get all categories data of the product

$get_current_productcats = array_slice($term_list_product_cat, 0, 3); //take the first 3 categories to which the product belongs.

Next, we will write a function that will return us the categories to which the product belongs in an array.

function current_child_productcat($arr_productcat){
	$productcats = ' ';
	foreach ($arr_productcat as $productcat) {
		$productcats .= $productcat->slug.',';
	}
	return explode(',',$productcats);
}

We have received all the necessary data now. Let’s write a needed wp_query, , on the basis of which we will be outputting related products.

$query = new WP_Query( array(  
	"post__not_in" => array( $post_id ), //do not forget to remove the current post from the results
	"post_type" => "product", //taking from the products
	"post_status" => "publish", //published
	"meta_key"  => "_regular_price", //sort by price
	"orderby" => "meta_value_num",
	"order" => "ASC",//from the cheapest to expensive
	"posts_per_page" => 3,//outputting three items
	"tax_query" => array( //filter by taxonomy
	    array(
			"taxonomy" => "product_cat",
			"field" => "slug",
			"terms" => current_child_productcat($get_current_productcats) //let's pass our array with categories
	    )
	)
) );
Our query is ready. Now we will take a part of the code from content-product.php in the woocommerce folder, it is responsible for the appearance of the products loop.

For reference, the code from your woocommerce folder may differ from mine, I have lots of custom hooks in my theme, and the layout is different too. So use wisely.

<?php if ( $query->have_posts() ) : ?>

<div class="single-page__related">
	<h3>Похожие товары</h3>

	<?php

	?>
	<ul class="block-grid list-unstyled row clearfix row-flex row-flex-wrap">
<?php
	while ( $query->have_posts() ) {
		$query->the_post();
?>

		<li class="col-lg-4 col-md-4 col-sm-6 col-xs-12 pull-left equal-height item product type-product status-publish has-post-thumbnail instock purchasable product-type-simple" >
			<div class="products-entry item-wrap clearfix">
				<div class="item-detail loading">
					<div class="item-img products-thumb">
						<?php
							/**
							 * woocommerce_before_shop_loop_item_title hook
							 *
							 * @hooked woocommerce_show_product_loop_sale_flash - 10
							 * @hooked woocommerce_template_loop_product_thumbnail - 10
							 */
							do_action( 'woocommerce_before_shop_loop_item_title' );
							
						?>
					</div>
					<div class="item-content products-content">
						<div class="item-loop__fixed-height">
						<?php
							/**
							 * woocommerce_shop_loop_item_title hook
							 *
							 * @hooked woocommerce_template_loop_product_title - 10
							 */
							do_action( 'woocommerce_shop_loop_item_title' );

							/**
							 * woocommerce_after_shop_loop_item_title hook
							 *
							 * @hooked woocommerce_template_loop_rating - 5
							 * @hooked woocommerce_template_loop_price - 10
							 */
							//do_action( 'onemall_template_loop_price' );
							do_action( 'woocommerce_after_shop_loop_item_title' );
						?>
						</div>
						<?php
							/**
							 * woocommerce_after_shop_loop_item hook
							 *
							 * @hooked woocommerce_template_loop_add_to_cart - 10
							 */
							do_action( 'woocommerce_after_shop_loop_item' );
						?>

					</div>
				</div>
			</div>
		</li>

<?php
	}
?>	
	</ul>
</div>

<?php endif; ?>
Ready. Now is time to include our custom output of related products to content-single-product.php inside the store templates.
<?php get_template_part( 'content-product-related' ); ?>

In my case, this code section I put after do_action('woocommerce_after_single_product_summary' );.

Cleaning the database from the garbage and optimize the tables

To solve this, I recommend the Advanced Database Cleaner. And its PRO version. If the site is old or in development period you turned on/off lots of plugins, you need it.

The plugin can do everything: find unused tables, options, optimize tables, delete revisions. I recommend to check directly all the tabs and carry out the necessary tasks. Especially a lot of attention to the orphan options, these are the settings which were created by all plugins, including those that have been removed a long time ago.

Unused options that are in the table wp_options. could make an extra load and use a lot of memory on every website request.

Above you can see an example of options with lots of orphan plugins. It is a good reason to clean them.

A little comment

For reference, the search logic of the orphan options isn’t perfect. Sometimes, there are quite living instances. Thus, do not delete anything there, if you have not made a full backup of MySQL. As for me, I generally deploy a test database with the site, delete options, test for a few days the result and only then do exactly the same steps in the production. There have been some troubles in the past 🙂

To be continued

On the database clearing, I will finish the article about the first optimizing steps. These methods are already enough to remove a decent level of the load from the website. In the next article of the cycle, I will tell you about how to make WP Supercache work with dynamic blocks for unauthorized users.