Скорость загрузки сайта влияет на конверсию. Скорость загрузки страниц влияет на их ранжирование в поиске Google и Яндекс. Известны случаи увеличения количества заказов на 40% или увеличения выручки на 13% в результате ускорения загрузки сайта. Наша команда регулярно выполняет работы по оптимизации Drupal-сайтов и серверов для ускорения загрузки сайтов на Drupal.
Одной из ключевых метрик скорости загрузки сайта является время генерации страницы веб-сервером. В работе по оптимизации сайта и сервера для уменьшения времени генерации страниц участвуют наши системные администраторы и специалисты DevOps, разработчики, специалисты со стороны заказчика и хостинг-провайдера. Мы используем профилировку XDebug медленных страниц, и анализ времени выполнения отдельных функций PHP для поиска и устранения узких мест в коде сайта. Применяем сервис мониторинга NewRelic для сбора агрегированной статистики времени обработки запроса загрузки страниц сервером. Анализируем статистику выполнения запросов к MySQL и Solr.
Публикуем кейс ускорения времени обработки запроса загрузки страниц интернет-магазина на 32% после решения распространенной проблемы для сайтов на Drupal.
Проблема
Есть в Drupal 7 давняя особенность - форма автоматически кэшируется при использовании AJAX фреймворка. При каждом выводе такой формы в таблице cache_form создаётся две записи (одна для структуры формы и одна для её состояния). Кэширование необходимо для корректной работы AJAX обработчика, которому нужно знать структуру и последнее состояние формы.
В случае, когда таких форм на странице много, возникает проблема быстрого роста количества записей в таблице cache_form. Особенно остро эта проблема проявляется при использовании AJAX кнопки "Добавить в корзину" в Commerce. К примеру, вывод 50 товаров на странице каталога приведёт к созданию 100 записей в таблице cache_form при каждом просмотре страницы. На высокопосещаемом сайте это приводит к тому, что таблица cache_form может иметь размер в несколько десятков гигабайт.
Также стоит учитывать тот факт, что кэш форм в Drupal 7 тесно связан с кэшем страниц и с минимальным временем жизни кэша (задаётся на странице Производительность). Если удалить кэш формы раньше, чем кэш страницы, возникнет ошибка "Некорректные POST-данные формы" при взаимодействии с формой. Если задать минимальное время жизни кэша, устаревший кэш форм не будет очищаться в течение установленного времени.
Для исправления вышеуказанной проблемы уже существует несколько решений:
- Переменная form_cache_expiration. Данная переменная была добавлена в версии 7.61 и позволяет управлять временем хранения кэша форм, которое по умолчанию равно 6 часам. Основной недостаток заключается в сильной зависимости от механизма очистки устаревшего кэша, без своевременного вмешательства которого, размер cache_form будет продолжать расти;
- Модуль OptimizeDB. Позволяет гибко настроить очистку таблицы cache_form по cron. Можно задать полную очистку таблицы или очистку только устаревших записей. Может возникать ошибка "Некорректные POST-данные формы", размер таблицы cache_form всё равно может остаться большим;
- Модуль Safe cache_form Clear. Предоставляет Drush команду для очистки устаревших записей в таблице cache_form. Недостатки аналогичны предыдущему модулю;
- Модуль Commerce Fast Ajax Add to Cart. Решение от xandeadx, направленное на корень проблемы - кэширование стандартной формы Drupal Commerce добавления в корзину. Минус решения, как ни странно, в том, что AJAX фреймворк не используется, а для нашего проекта разработчиками уже был написан нестандартный диалог добавления в корзину с использованием AJAX-команд Drupal. Кроме того, это решение не универсально и работает только для формы добавления товара в корзину;
- Патч #94 из этого issue. Кроме применения патча потребуется дописать обработчик для нужной формы. Это решение может работать нестабильно для страниц с множественным выводом форм. Не работает для страниц со случайным списком товаров. Ну и уже большой минус в том, что надо патчить ядро.
Решение
Для примера возьмём чистую установку Drupal 7.67 с модулем Commerce 1.15. Реализуем страницы каталога с помощью Views. На каждой странице выведем по 50 товаров, в каждом тизере товара выведем кнопку добавления в корзину. Для удобства сгенерируем товары с помощью модуля Commerce Devel. Для AJAX-ификации кнопки добавления товара в корзину используем модуль Commerce Ajax Add to Cart. Открываем страницу каталога и проверяем - в таблице cache_form появилось 100 новых записей, проблема воспроизведена.
Решение, предлагаемое в данной статье, частично основывается на данном комментарии. Для его реализации потребуется создать небольшой кастомный модуль или добавить код в уже имеющийся. В нашем примере это будет модуль custom.
Первым делом определим путь для нового AJAX-обработчика формы. По своей структуре он похож на определение пути "system/ajax" в модуле system.
/**
* Implements hook_menu().
*/
function custom_menu() {
$items['custom/form/ajax'] = array(
'title' => 'AJAX callback',
'page callback' => 'custom_form_ajax_callback',
'delivery callback' => 'ajax_deliver',
'access arguments' => array('access content'),
'theme callback' => 'ajax_base_page_theme',
'type' => MENU_CALLBACK,
);
return $items;
}
Изменим путь AJAX-обработчика у кнопки добавления товара в корзину (свойство path). Здесь важно не путать свойства path и callback - первое определяет адрес, на который будет отправлен AJAX-запрос, а второе указывает функцию, которая при этом запросе будет вызвана для формирования ответа. Как правило, path не указывают и берётся значение по умолчанию "system/ajax", его и требуется поменять. Также принудительно отключим кэширование интересующей нас формы.
/**
* Implements hook_form_alter().
*/
function custom_form_alter(&$form, &$form_state, $form_id) {
if (strpos($form_id, 'commerce_cart_add_to_cart_form') !== FALSE) {
// Указываем, что хотим самостоятельно обработать AJAX-запрос к форме.
$form['submit']['#ajax']['path'] = 'custom/form/ajax';
// Отключаем кэширование формы.
$form_state['no_cache'] = TRUE;
}
}
Наконец, реализуем функцию custom_form_ajax_callback(), которую ранее указали в определении пути "custom/form/ajax". Код функции частично повторяет код функций ajax_get_form() и ajax_form_callback(). Основная идея заключается в том, что нужно получить правильное состояние формы без использования кэша, так как мы его уже отключили. Важно отметить, что приведённый далее код универсален и может быть применен для отключения кеширования других AJAX-форм, за исключением блока, в котором выполняется формирование товарной позиции. Именно в данном блоке происходит построение состояния формы, необходимого для корректной валидации и сабмита. Для поддержки атрибутов товаров потребуется доработка этого кода. Для других форм потребуется написать аналогичный код.
/**
* Menu callback; handles Ajax requests for forms without caching.
*
* @return array|null
* Array of ajax commands or NULL on failure.
*/
function custom_form_ajax_callback() {
// Проверяем, что обрабатываем AJAX-запрос к форме.
if (isset($_POST['form_id']) && isset($_POST['form_build_id'])) {
$form_build_id = $_POST['form_build_id'];
$form_id = $_POST['form_id'];
$commands = array();
// Инициализируем состояние формы.
$form_state = form_state_defaults();
$form_state['build_info']['args'] = array();
// Заполняем состояние формы. Данный код уникален в рамках обрабатываемой формы.
// Проверяем, что форма является формой добавления товара в корзину.
if (strpos($form_id, 'commerce_cart_add_to_cart_form_') === 0) {
$product = commerce_product_load($_POST['product_id']);
if (!empty($product)) {
// Формируем сущность товарной позиции на основе данных отправленной формы.
$line_item = commerce_product_line_item_new($product, $_POST['quantity'] ?? 1);
$line_item->data['context']['product_ids'] = array($product->product_id);
$line_item->data['context']['add_to_cart_combine'] = TRUE;
// Добавляем товарную позицию в состояние формы.
$form_state['build_info']['args'] = array($line_item);
}
}
// Строим форму, будут вызваны билдеры и соответствующие хуки.
$form = drupal_retrieve_form($form_id, $form_state);
drupal_prepare_form($form_id, $form, $form_state);
$form['#build_id_old'] = $form_build_id;
// Обрабатываем форму аналогично тому, как это сделано в ajax_get_form().
if ($form['#build_id_old'] != $form['#build_id']) {
$commands[] = ajax_command_update_build_id($form);
}
$form_state['no_redirect'] = TRUE;
$form_state['rebuild_info']['copy']['#build_id'] = TRUE;
$form_state['rebuild_info']['copy']['#action'] = TRUE;
$form_state['input'] = $_POST;
// Обрабатываем форму аналогично тому, как это сделано в ajax_form_callback().
drupal_process_form($form['#form_id'], $form, $form_state);
if (!empty($form_state['triggering_element'])) {
$callback = $form_state['triggering_element']['#ajax']['callback'];
}
if (!empty($callback) && is_callable($callback)) {
$result = $callback($form, $form_state);
if (!(is_array($result) && isset($result['#type']) && $result['#type'] == 'ajax')) {
$result = array(
'#type' => 'ajax',
'#commands' => ajax_prepare_response($result),
);
}
$result['#commands'] = array_merge($commands, $result['#commands']);
return $result;
}
}
return NULL;
}
Результаты
В нашем примере добавление вышеуказанного кода приводит к сохранению AJAX-функционала кнопки добавления товара в корзину при отключённом кэшировании формы. Обновление страницы каталога более не приводит к созданию 100 записей в таблице cache_form. Результат обработки AJAX-запроса аналогичен результату обработки при использовании кэша. Изменения формы, добавленные в рамках Drupal API (например, атрибуты товаров) либо потребуют небольших изменений в коде (построения состояния формы), либо не потребуют их вообще.
Кроме того, интересен результат применения решения на реальном проекте, для которого оно и было реализовано:
- Количество SELECT запросов к таблице cache_form уменьшилось в 10 раз;
- Количество INSERT запросов к таблице cache_form уменьшилось в 10 раз;
- Среднее время обработки запроса сервером уменьшилось на 32% (с 352 до 241 миллисекунд).
Более подробно статистика отображена на скриншотах.






Как видно, среднее время от запроса до ответа сервера APP SERVER уменьшилось с 352 до 241 миллисекунд. Размер таблицы cache_form уменьшился с ~10Гб до 200Мб.
Дальнейшие доработки других форм сайта аналогичным способом позволят улучшить еще эти показатели.
Добавить комментарий