Продвинутая оптимизация WordPress. Часть 1: основные моменты

В статье о «Шустром WordPress» я рассказывал об ускорении сайта через генерацию HTML страниц и отдачи их в виде статики с помощью кэширующего сервера Varnish. Но бывает и так, что сайт испытывает большую нагрузку от авторизованных пользователей или есть отдельные части, которые кэшировать нежелательно.

Например, если это магазин на Woocommerce с более чем 10 000 товарами и расширенным личным кабинетом, всяческими многоуровневыми скидочными программами и тому подобным.

В ситуации с динамической нагрузкой придется применять более продвинутые, и, в некоторой степени, индивидуальные приемы оптимизации. Именно поэтому, я начинаю цикл статей, в котором попробую описать все возможные шаги, которые мне приходилось применять. А вы уже примените нужные с поправкой на свою ситуацию.
Артем Зайцев
Evilmarketer

Выключаем WP_Cron

WP_Cron на сайтах, обвешанных плагинами, создает порой нехилую нагрузку. Встроенный планировщик задач на WordPress работает таким образом, что при каждой загрузке страниц, проходят запросы к wp-cron.php. Каждый такой запрос проверяет, нет ли задач к выполнению на данное время. Естественно, что такой механизм создает дополнительную ненужную нагрузку, которую можно и нужно убрать. 

Чтобы лишнюю нагрузку убрать, а wp_cron при этом функционировал, мы его отключим и будем обращаться к файлу wp-cron.php через cron на сервере:
Как выключить
Чтобы отключить обращение к WP-cron надо в wp-config.php прописать следующую строчку
define('DISABLE_WP_CRON', true);
В панели D2C, в сервисе, который отвечает за само приложение мы создадим cron задачу на обращение к wp-cron.php каждые 5 минут:
#используя cURL. Он доступен «из коробки»
*/5 * * * *
curl https://yoursite.com/wp-cron.php?doing_wp_cron

Выглядит в панели это примерно так:
*/5 * * * *
wget -O - -q -t 1 https://yoursite.com/wp-cron.php?doing_wp_cron

Параметр -O выводит в консоль вместо сохранения в файл; -q делает это тихо, без вывода на экран; -t 1 указывает делать лишь одну попытку соединения.
Выглядит в панели это примерно так:

Включаем объектный кэш

Объектный кэш позволяет сохранять данные произвольного типа, и получать эти данные при запросе. Если добавить его к нашему стеку, большую часть данных мы сможем получать, исключив обращения к MySQL. Это повысит производительность системы.

В WordPresse можно использовать 2 решения: Memcached и Redis. Чтобы включить Memcached, потребуется плагин. Я использую на проектах Memcached Redux.

Как подключить

Для начала нужно добавить сервис в панели D2C и скопировать alias с портом. Он понадобится нам позже.

Чтобы настроить WordPress на работу с Memcached, надо файл object-cache.php из плагина Memcached Redux положить в папку wp-content и указать в wp-config.php массив с адресами серверов кэша. Сам плагин при этом ставить в папку plugins не нужно.
В моем случае присутствует единственный сервис с алиасом dokmemcached, его и укажу в конфиге:
$memcached_servers = array('dokmemcached:11211');
Если передать в массиве несколько адресов, WordPress будет работать и с ними.
Помимо алиасов, не лишним будет указать уникальный WP_CACHE_KEY_SALT. Эта константа нужна для того, чтобы записи с разных сайтов, обращающихся к одному кэширующему серверу, не конфликтовали. Можно указывать что угодно, но я предпочитаю указывать название сайта:
define('WP_CACHE_KEY_SALT', 'dokmarket');
Если вы все сделали правильно, сайт заработает. Если допустили ошибки, увидите 500 ошибку.
Эффективность
Использование memcached в разы снижает количество обращений к базе. Сами же данные объектного кэша хранятся в оперативной памяти, что дает сильное преимущество в скорости доступа к ним. Пример все с того же сайта.
Страница товара без объектного кэша
SQL: 209 за 1.000 sec.

Страница товара с объектным кэшем
SQL: 45 за 0.000 sec.

Выводим похожие товары без использования woocommerce_related_products()

Решение для обладателей объемного магазина на Woocommerce.
У woocommerce есть собственная функция woocommerce_related_products(), она используется для реализации блока похожих товаров. Её проблема в том, что запросы к выводу сопутствующей продукции Woocommerce делает крайне затратным способом: подтягивает кучу метаданных, ищет по всем товарам и потом выводит указанное количество рандомно, используя ‘orderby’ => ‘rand’ в wp_query.

‘orderby’ => ‘rand’ надо переписывать нещадно и везде. Этот метод, наверное, один из самых прожорливых в WP. На моей практике был случай, когда подобный способ генерировал аж под 1000 запросов к базе.

Есть 2 варианта решения проблемы: поставить дополнительный слой в стек в виде Elasticsearch и направлять подобные запросы к нему или изменить логику вывода похожих товаров на менее затратный способ. В параграфе я рассмотрю только второй вариант.

Прежде чем реализовать задумку, надо определиться с логикой определения похожих товаров. Если посмотреть с точки зрения пользователя, то видеть рандомные товары среди похожих не особо полезно. В этом случае имеет смысл вывести их по популярности (количеству продаж), рейтингу или стоимости (от дешевого к дорогому). Если проект новый и не накоплено достаточно статистики по продажам или не собрано большого количества отзывов, я использую фильтр по цене. Так и поступим.

Реализация
Для начала создадим пустой файл в папке с активной темой, назовем, его, к примеру, content-product-related.php.
Выше вы можете наблюдать папку “woocommerce”, там лежат шаблоны магазина. Если в вашей текущей теме её нет, можно создать и скопировать файлы из папки templates плагина Woocommerce. Так вы сможете управлять шаблонами магазина непосредственного из темы.
Итак, работаем с фалом content-product-related.php. Прежде чем приступить непосредственно к написанию кода, запретим в шапке прямой доступ.
if ( ! defined( 'ABSPATH' ) ) exit; 
Далее необходимо вызвать глобальную переменную $products и убедиться, что мы находимся на странице продукта:
global $product;
if ( ! $product || ! $product->is_visible() ) {
	return;
}
Также нам понадобятся некоторые данные о продукте, к которому должны относится результаты вывода похожих товаров. Это будет ID товара, и информацию по категориям, к которым относится товар.
$post_id = get_the_ID();
$term_list_product_cat = wp_get_post_terms( $post_id, 'product_cat', array("fields" => "all") );//здесь получаю все данные по категориям, которые относятся к данному товару

$get_current_productcats = array_slice($term_list_product_cat, 0, 3); //выбираю первые 3 категории, к которым относится продукт, можно увеличить уровень вложенности или выбрать вообще все. Лично мне достаточно сопоставлять по трем.
Далее напишем функцию, которая возвратит нам категории, к которым относится товар в виде массива.
function current_child_productcat($arr_productcat){
	$productcats = ' ';
	foreach ($arr_productcat as $productcat) {
		$productcats .= $productcat->slug.',';
	}
	return explode(',',$productcats);
}
Все, нужные данные мы получили, теперь приступим к формированию запроса wp_query, на основе которого будем выводить похожие товары.
$query = new WP_Query( array(  
	"post__not_in" => array( $post_id ), //не забудем убрать текущий пост из результатов
	"post_type" => "product", //брать будем из товаров
	"post_status" => "publish", //опубликованных
	"meta_key"  => "_regular_price", //сортировать по цене
	"orderby" => "meta_value_num",
	"order" => "ASC",//от дешевой к дорогой
	"posts_per_page" => 3,//выведем три результата
	"tax_query" => array( //отфильтруем по таксономии
	    array(
			"taxonomy" => "product_cat",
			"field" => "slug",
			"terms" => current_child_productcat($get_current_productcats) //передадим наш массив с категориями
	    )
	)
) );
Наш запрос готов, приступим к выводу похожих постов. Для этого возьмем часть кода из content-product.php из папки woocommerce, он отвечает за внешний вид таблицы в листинге товаров.
Сразу оговорюсь, код из вашей папки woocomerce может отличаться от моего, так как у меня в теме многие хуки вордпресса изменены на кастомные, да и верстка отличается. Применяйте с умом.
<?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; ?>
Готово, теперь подключим наш кастомный вывод похожих товаров в content-single-product.php внутри шаблонов магазина, он отвечает за внешний вид товара.
<?php get_template_part( 'content-product-related' ); ?>
В моем случае этот участок кода я поставил после do_action( 'woocommerce_after_single_product_summary' );.

Очищаем базу данных от ненужного мусора и оптимизируем таблицы

Для решения этой задачи я рекомендую плагин Advanced Database Cleaner. Причем его PRO версию. Если сайт существует давно или вы в период разработки подключали-отключали кучу плагинов, он вам сильно поможет. 

Плагин умеет все: находить неиспользуемые таблицы, опции, оптимизировать таблицы, удалять ревизии. Рекомендую пройтись прямо по всем вкладкам и провести необходимые процедуры. Особенно много внимание уделите неиспользуемым опциям (orphan options), это те самые настройки, которые насоздавали все плагины без исключения, в том числе и те, что давно уже удалены.

Неиспользуемые опции, которые находятся в таблице wp_options, могут создавать нехилую нагрузку и пожирать кучу памяти. Так происходит потому, что настройки со значением autoload подгружаются при каждом обращении к WordPress.

Выше вы можете наблюдать пример опций уже давно неиспользуемых плагинов, которые продолжают постоянно подгружаться. Есть хороший повод их почистить.
Ремарка
Сразу говорю, механизм поиска неиспользуемых настроек не идеален. Бывает, туда попадают вполне себе живые экземпляры. Поэтому ни в коем случае ничего здесь не удаляйте, если не сделали полный бэкап MySQL. Лично я вообще поднимаю тестовую базу с сайтом, удаляю там, тестирую в течение нескольких дней результат и только потом в продакшене проделываю точно такие же действия. Бывали неприятности в прошлом, поэтому теперь я немного параноик 🙂

 

Продолжение следует

На очистке БД от мусора я закончу рассказ о первых шагах в оптимизации WordPress. Перечисленных методов уже достаточно, чтобы снять приличный пласт нагрузки с сайта. В следующей статье цикла расскажу о генерации статичного кэша с помощью плагина WP Supercache и настройке динамических блоков для неавторизованных пользователей.