/sites/default/files/2019-09/EDf41xbXkAEQRn8_0.jpg

Скорость загрузки сайта влияет на конверсию. Скорость загрузки страниц влияет на их ранжирование в поиске 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 миллисекунд).

Более подробно статистика отображена на скриншотах.

Скриншот 1

 

 

Скриншот 2

 

 

Скриншот 3

 

 

Скриншот 4

 

 

Скриншот 5

 

Скриншот 6

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

Дальнейшие доработки других форм сайта аналогичным способом позволят улучшить еще эти показатели.

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