Скорость загрузки сайта влияет на конверсию. Скорость загрузки страниц влияет на их ранжирование в поиске 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Мб.
Дальнейшие доработки других форм сайта аналогичным способом позволят улучшить еще эти показатели.
Добавить комментарий