Магазин тормозит: что чинить первым на каталоге в 20 000 товаров
Владелец магазина прислал скриншот из PageSpeed Insights с оценкой 34 балла и вопросом «что за ужас, всё почините». Разбираю, что на самом деле стоит за низким баллом на большом каталоге — база данных, зоопарк плагинов, кеш, который не лечит корзину, и внешние шрифты, которые бьют по скорости и приватности одновременно.
COS / KNOWLEDGE BASE
# Магазин тормозит: что чинить первым на каталоге в 20 000 товаров
Мне прислали скриншот в мессенджер без единого слова — просто отчёт PageSpeed Insights с огромной красной цифрой 34 наверху и минутой спустя сообщение: «Это что за кошмар, почините уже». Магазин строительных материалов, каталог за двадцать тысяч позиций, мобильная версия, тот самый большой красный круг, который любой владелец бизнеса, хоть немного следящий за темой SEO, воспринимает как приговор. И знаете, что было первой мыслью, когда я открыл отчёт? Что цифра 34 сама по себе ничего не объясняет. Она просто сигнализирует «что-то не так», но не говорит, что именно — а без этого понимания можно бесконечно оптимизировать не то, что нужно, и ронять деньги в мусорную корзину под видом технической работы.
Это, кстати, довольно частая история — предприниматель видит балл в Lighthouse или PageSpeed Insights, впадает в панику, находит первого попавшегося специалиста, который обещает «поднять скорость до зелёной зоны», и тот начинает крутить настройки кеша, сжимать картинки, вычищать неиспользуемый CSS, попутно выставляя счёт за несколько часов работы, которые владельцу бизнеса подаются как решение проблемы целиком, хотя на деле это лишь один из четырёх фронтов, да ещё и не факт что самый важный именно для этого конкретного магазина. Иногда это даже немного помогает — балл ползёт вверх, скажем, с 34 до 52. Владелец бизнеса радуется, потому что цифра выросла, а через месяц выясняется, что конверсия не изменилась ни на процент, потому что настоящая причина медленной работы сайта вообще не имела отношения к тому, что чинили.
За годы работы с магазинами на больших каталогах я вывел для себя простое правило: если сайт «тормозит», причин почти никогда не одна. Обычно их четыре, и они складываются друг с другом, усиливая эффект. База данных, разросшаяся под тяжестью годами копившихся товаров и мета-данных. Зоопарк плагинов, каждый из которых незаметно добавляет свою нагрузку. Кеш, настроенный неправильно или не покрывающий именно те страницы, где происходят реальные покупки. И внешние ресурсы — шрифты, скрипты, изображения — которые тянутся с чужих серверов и тормозят загрузку по причинам, которые вообще не зависят от вашего хостинга.
Давайте разберём каждую из этих четырёх причин по отдельности, потому что лечить их нужно совершенно разными инструментами, и начинать нужно не с той, что проще всего чинить, а с той, что реально стоит между покупателем и оформленным заказом.
База данных: где остатки, атрибуты и вариации превращаются в тормоз
Начнём с самого фундаментального и самого недооценённого источника проблем — устройства базы данных WordPress и то, как WooCommerce хранит информацию о товарах. Ядро WordPress исторически спроектировано вокруг таблицы `wp_postmeta` — это универсальное хранилище дополнительных данных практически для чего угодно, устроенное по модели ключ-значение: идентификатор записи, название параметра, значение параметра. Для блога с текстовыми постами это работает прекрасно. Для интернет-магазина, где у каждого товара есть цена, артикул, вес, набор атрибутов, привязка к вариациям, метаданные наличия и десяток других параметров, эта же таблица начинает расти взрывными темпами и превращается в самое узкое место всей системы.
Я видел проект, где таблица `wp_postmeta` разрослась почти до гигабайта — и это при каталоге, который сам по себе не выглядел таким уж чудовищным по меркам крупного ретейла. Просто накопились годы истории: старые значения атрибутов от давно снятых с продажи вариаций, дублирующиеся записи от нескольких волн импорта, метаданные от плагинов, которые давно отключили, но чьи данные никто не почистил. База данных не забывает ничего сама — она просто пассивно копит, а расплачивается за это производительность каждого запроса, который так или иначе обращается к этой таблице, включая, что особенно неприятно, практически любую фильтрацию каталога по цене, наличию или атрибутам.
Проблема усугубляется тем, как устроен сам механизм выборки. Когда покупатель заходит на страницу категории и включает фильтр «в наличии, от 500 до 2000 рублей, серый цвет», WordPress формирует SQL-запрос, который должен присоединить таблицу постметы несколько раз — один раз для проверки наличия, второй для цены, третий для атрибута цвета. На маленьком каталоге в пару сотен товаров такой запрос выполняется мгновенно. На двадцати тысячах товаров с несколькими вариациями у каждого — это уже тяжёлая операция, которая при отсутствии правильных индексов в таблице превращается в полное сканирование миллионов строк.
Хорошая новость в том, что сама команда WooCommerce давно осознала эту проблему и добавила вспомогательные инструменты. Есть отдельная таблица подстановки — она хранит цену, наличие и ключевые параметры товара в удобной для быстрой выборки форме, чтобы не лазить каждый раз в постметы. Есть относительно недавно появившаяся архитектура хранения заказов отдельно от основной таблицы записей, которая ощутимо разгружает базу на магазинах с большой историей продаж — раньше каждый заказ хранился как ещё одна запись в той же таблице, где хранятся товары, страницы и посты блога, и на магазине с многолетней историей эта таблица тоже раздувалась неприлично. Плохая новость в том, что оба этих механизма нужно осознанно включить и следить за их состоянием — они не активируются сами по себе просто от установки WooCommerce, и я регулярно вижу магазины, которые технически уже несколько лет могли бы работать быстрее, просто никто не удосужился включить нужную настройку и запустить регенерацию вспомогательных таблиц.
Ещё одна классическая находка — отсутствующие или устаревшие индексы непосредственно в самих таблицах MySQL. Индекс — это структура данных, которая позволяет базе находить нужные строки, не перебирая всю таблицу целиком, примерно как алфавитный указатель в конце книги вместо построчного чтения всего текста в поисках одного слова. Когда каталог растёт органически годами, через миграции с других платформ, через ручные правки в базе, индексы нередко оказываются либо не проставлены на нужных полях, либо фрагментированы настолько, что база данных перестаёт их эффективно использовать. Диагностируется это довольно просто через анализ медленных запросов на уровне MySQL — большинство хостингов дают доступ к логу медленных запросов, и просмотр этого лога за неделю обычно сразу показывает три-пять запросов, которые выполняются секундами вместо миллисекунд, и почти всегда это запросы, связанные с фильтрацией и сортировкой каталога.
Диагностировать, где именно происходит затык, на самом деле не так сложно, как кажется владельцу бизнеса без технического бэкграунда — просто это редко делают методично. Практически любой сервер MySQL можно попросить объяснить, каким образом он выполняет конкретный запрос, — это называется планом выполнения, и он показывает открытым текстом, читает ли база нужные строки через индекс или вынуждена перебирать таблицу целиком построчно. Если в плане выполнения фигурирует полное сканирование таблицы на сотнях тысяч строк постметы — вот она, причина торможения, найденная за одну команду, а не за недели гадания. Второй столь же простой инструмент — включённый лог медленных запросов, который сам, без всякого вмешательства, копит список операций, занявших больше заданного порога времени, и через неделю работы такого лога обычно видно три-пять повторяющихся паттернов запросов, отвечающих за большую часть проблем.
Есть ещё один слой, который часто упускают, говоря о базе данных, — это объектное кеширование на уровне самого сервера, обычно реализуемое через Redis или Memcached. WordPress по умолчанию кеширует часть повторяющихся запросов только в пределах одной загрузки страницы — то есть при следующем визите того же самого посетителя за той же самой информацией всё придётся запрашивать заново. Объектный кеш, настроенный на уровне сервера, хранит результаты частых запросов между визитами разных посетителей — список категорий, настройки виджетов, результаты тяжёлых агрегаций — и раздаёт их из оперативной памяти вместо повторного обращения к диску, где физически лежит база данных. На каталоге в двадцать тысяч товаров с активным трафиком включение объектного кеша нередко даёт настолько же заметный эффект, как оптимизация самих запросов, просто потому что многие из этих запросов в принципе не обязаны выполняться заново при каждом визите нового посетителя.
И вот здесь я подхожу к теме, ради которой, честно говоря, отчасти была задумана вся архитектура нашего модуля поиска — если фильтрация и поиск по каталогу настолько сильно упираются в ограничения MySQL, разумный шаг — вообще вынести эту нагрузку за пределы основной базы данных. Мы в COS WP Woo для этого используем отдельный поисковый движок Typesense, который хранит индексированную копию каталога в оптимизированной для быстрого поиска структуре и отвечает на запросы за единицы миллисекунд вместо секунд, полностью снимая эту нагрузку с MySQL. Я подробно разбирал архитектуру и реальные цифры производительности такого подхода в статье «Умный поиск на Typesense: быстрее встроенного WooCommerce в 50 раз» — там же приведён конкретный пример магазина с шестнадцатью тысячами товаров, где среднее время ответа упало с 800-2000 миллисекунд до 8-15 миллисекунд именно за счёт переноса поисковой нагрузки на специализированный инструмент.
Двадцать плагинов — это не список функций, это архитектурная проблема
Вторая по значимости причина, с которой я сталкиваюсь практически в каждом немолодом WooCommerce-магазине, — это накопленный за годы зоопарк плагинов. История типична: магазин запустили с десятком базовых расширений, потом понадобилась функция сравнения товаров — поставили плагин. Потом захотели вишлист — ещё один плагин. Потом попросили мегаменю покрасивее — третий. Через три-четыре года в панели администратора активно от пятнадцати до тридцати плагинов, половина из которых установлена «на попробовать» и с тех пор ни разу не пересматривалась.
Каждый плагин, даже самый скромный по функциональности, добавляет свою цену на каждой странице сайта — минимум один дополнительный HTTP-запрос на подключение своего файла стилей, ещё один на файл скриптов, а нередко и отдельный запрос к базе данных на получение своих настроек при каждой загрузке страницы. По отдельности это кажется мелочью — ну какие-то лишние двадцать-тридцать миллисекунд, кто их заметит. Но когда таких плагинов двадцать, эти мелочи складываются в секунды суммарной задержки, причём складываются не линейно, а зачастую хуже, потому что плагины начинают конфликтовать друг с другом за одни и те же хуки WordPress, за одни и те же события, иногда буквально перезаписывая результаты работы друг друга и заставляя часть кода выполняться повторно.
У нас в проекте есть реальные цифры, которые хорошо иллюстрируют масштаб проблемы просто на уровне фронтенд-ассетов, без всякой базы данных. Когда мы навели порядок в наборе файлов стилей и скриптов на витрине одного из проектов — просто минифицировали и собрали воедино то, что раньше подключалось десятком разрозненных файлов от разных модулей, — суммарный вес этих файлов сократился почти на сорок процентов, с 551 килобайта до 333 при сорока пяти отдельных файлах. Причём это всего лишь минификация без изменения логики — просто убрали комментарии, пробелы, привели код к компактному виду. Отдельные файлы сократились ещё сильнее: один из скриптов фильтрации похудел на шестьдесят процентов, скрипт страницы результатов поиска — почти на половину. И это без учёта того, что каждый из сорока пяти файлов — это отдельный сетевой запрос браузера, а не просто байты в сумме.
Теперь представьте это же на магазине, где вместо одного продуманного набора модулей стоит два десятка независимых плагинов от разных разработчиков, ни один из которых не в курсе, что рядом работают ещё девятнадцать таких же. Никто не минифицировал их совместно, никто не объединял запросы, никто не проверял, не подключает ли один и тот же скрипт jQuery-плагин дважды с разными версиями. Я видел магазины, где суммарный вес JS и CSS на витрине превышал два мегабайта только за счёт накопленных плагинов, и это без единой фотографии товара — просто код интерфейса.
Есть отдельная категория проблем, возникающая именно из-за конфликта плагинов на уровне запросов к базе данных. Представьте плагин вишлиста, который на каждой странице каталога делает отдельный запрос — есть ли этот товар в списке избранного у текущего пользователя. Рядом плагин сравнения товаров делает то же самое для своей таблицы. Рядом плагин показа «похожих товаров» на каждой карточке делает ещё десяток дополнительных запросов к базе, выбирая похожие позиции по категории. По отдельности каждый из них терпим. Вместе на странице категории с сорока товарами в листинге они могут суммарно генерировать несколько сотен дополнительных запросов к базе данных на одну загрузку страницы — и вот это уже совершенно другая история, потому что современный сервер физически ограничен в том, сколько запросов он способен обработать за разумное время, особенно если несколько посетителей заходят на сайт одновременно.
Есть и менее очевидная сторона проблемы зоопарка плагинов — риск, связанный с обновлениями. У каждого плагина свой график выпуска новых версий, свой автор, своя скорость реакции на изменения в ядре WordPress или WooCommerce. Когда таких независимых плагинов двадцать, вероятность, что хотя бы один из них не успеет вовремя адаптироваться к очередному крупному обновлению платформы, становится не теоретической, а практически неизбежной статистикой. И в этот момент владелец бизнеса оказывается перед неприятным выбором — либо откладывать важное обновление безопасности всего сайта из-за одного капризного плагина, либо обновляться и рисковать, что часть функциональности сломается прямо во время активных продаж. Я видел магазины, которые годами сидели на устаревшей версии WooCommerce именно по этой причине — не потому что не хотели обновляться, а потому что боялись сломать хрупкий баланс из двадцати плохо совместимых друг с другом расширений.
Отдельно скажу про плагины безопасности и аналитики, потому что они часто становятся невидимым источником нагрузки именно из-за своей полезности — владелец бизнеса меньше всего хочет их отключать, ведь они защищают магазин или считают статистику. Но многие такие плагины по своей природе выполняют дополнительную работу на каждой без исключения загрузке страницы — сканируют запрос на признаки атаки, записывают событие в лог посещений, сверяются с базой известных вредоносных IP-адресов. Это разумная и нужная работа, но она тоже стоит времени процессора, и когда таких проверяющих модулей несколько, а не один, суммарная накладная стоимость становится ощутимой. Мы у себя решаем это тем, что защитные механизмы — файрвол, блокировку ботов, журнал активности — держим внутри одного модуля с общим циклом обработки запроса вместо нескольких независимых проверок, которые дублируют часть работы друг друга; подробнее о самой архитектуре защиты я писал в статье про WAF, 2FA и геоблокировку для WooCommerce.
Именно поэтому мы в самом начале работы над COS WP Woo сделали ставку на объединение функциональности в единую систему, а не на добавление ещё одного независимого плагина под каждую отдельную задачу — эта же логика подробно разобрана в статье про зоопарк из двадцати WooCommerce-расширений, где я считаю не только производительность, но и деньги, которые уходят на подписки за этот зоопарк. С точки зрения скорости сайта единая архитектура даёт возможность переиспользовать одни и те же данные между модулями вместо дублирующих запросов, подключать общий набор стилей и скриптов один раз вместо двадцати раз, и, что немаловажно, тестировать совместимость компонентов друг с другом внутри одной кодовой базы, а не надеяться, что два независимых разработчика с разных концов света случайно не столкнутся своими хуками.
Кеш — это не волшебная таблетка: корзина, фильтры и личный кабинет
Третья причина торможения — это неправильные ожидания от кеширования. Почти на каждом WooCommerce-магазине рано или поздно появляется плагин полностраничного кеша — и это правильное, необходимое решение, которое действительно кардинально ускоряет отдачу страниц. Но у полностраничного кеша есть фундаментальное ограничение, о котором забывают: он хорошо работает только для страниц, которые выглядят одинаково для всех посетителей. Главная страница, страница категории без применённых фильтров, статья блога — отличные кандидаты для кеша, потому что их можно один раз отрендерить и потом отдавать этот же готовый HTML тысячам разных посетителей.
А вот корзина, личный кабинет, страница оформления заказа — это персонализированный контент, уникальный для каждого конкретного покупателя, и кешировать его целиком нельзя в принципе, иначе один покупатель увидит содержимое корзины другого. Значит, эти страницы либо полностью исключаются из кеша и обрабатываются сервером каждый раз с нуля, либо кешируются частично, с динамическими фрагментами, догружаемыми через дополнительный запрос уже после отдачи основной страницы. И вот здесь кроется классическая, невероятно распространённая проблема WooCommerce — механизм под названием «фрагменты корзины».
Штатная логика WooCommerce устроена так: даже когда страница целиком отдана из кеша, счётчик товаров в корзине и мини-корзина в шапке сайта должны быть актуальными именно для текущего посетителя. Для этого WooCommerce на каждой загрузке любой страницы сайта делает дополнительный AJAX-запрос к серверу с именем `get_refreshed_fragments`, который обновляет содержимое корзины поверх закешированной страницы. Звучит разумно, но на практике это означает, что даже на полностью закешированной главной странице сайт всё равно делает полноценный запрос к PHP и базе данных на каждой загрузке — то есть весь смысл кеширования частично нивелируется одним этим механизмом, о существовании которого большинство владельцев магазинов даже не подозревает.
Я разбирал десятки магазинов, где владелец гордо показывал отчёт «страница отдаётся из кеша за 40 миллисекунд», совершенно не замечая, что параллельно в фоне уходит запрос на обновление фрагментов корзины, который выполняется полторы секунды и создаёт полноценную нагрузку на сервер при каждом визите. При активном трафике эта фоновая нагрузка от, казалось бы, «второстепенного» запроса нередко оказывается больше, чем нагрузка от отдачи самих кешированных страниц. Лечится это осознанной настройкой — либо отключением автоматического обновления фрагментов на страницах, где корзина заведомо не нужна, либо переводом счётчика корзины на более лёгкий механизм, не требующий полной инициализации WooCommerce при каждом обращении.
Похожая история с фильтрами каталога. Если фильтрация реализована через классические ссылки с перезагрузкой страницы, каждая уникальная комбинация фильтров технически создаёт новый URL, и при активном использовании фильтров покупателями кеш начинает распухать от тысяч вариаций одной и той же по сути страницы категории, каждая из которых занимает место и требует отдельного прогрева. Ещё хуже, если фильтрация реализована через AJAX без изменения URL — тогда сам факт фильтрации вообще не попадает под полностраничный кеш, и каждое взаимодействие покупателя с фильтром бьёт напрямую в PHP и базу данных, снова и снова выполняя те самые тяжёлые запросы к постметам, о которых я говорил в начале статьи. Разумный баланс — предвычисленный индекс фильтров, который считает количество товаров по каждой комбинации заранее, в фоне, а не в момент клика покупателя, и AJAX-фильтрация поверх этого индекса, а не поверх прямых запросов к постметам. Я подробно показывал этот подход в статье про AJAX-фильтр товаров без перезагрузки страницы.
Стоит упомянуть и ещё один слой кеширования, который часто путают с полностраничным кешем плагина, — кеширование кода самого PHP на уровне сервера, обычно называемое OPcache. Без него сервер вынужден заново компилировать один и тот же PHP-код при каждом запросе, что добавляет ощутимые накладные расходы на процессор, особенно при большом количестве плагинов с их собственным кодом. Это настройка сервера, а не плагина, и её включение — обычно одна строка в конфигурации PHP, но по моим наблюдениям на удивление большой процент недорогих хостингов либо не включает её по умолчанию, либо выделяет под неё смехотворно маленький объём памяти, из-за чего кеш кода постоянно перезаписывается сам поверх себя вместо того, чтобы стабильно ускорять каждый последующий запрос.
Есть и третий классический пример того, что кеш не лечит, — личный кабинет и авторизованные разделы сайта, особенно если магазин работает с B2B-клиентами и у них есть персональные цены, история заказов, статус закупочного лимита. Эти страницы по определению уникальны для каждого пользователя и никогда не должны кешироваться целиком — значит, их скорость целиком зависит от того, насколько оптимизированы конкретные запросы за этими данными, и никакой универсальный плагин кеширования эту работу за вас не сделает. Если личный кабинет B2B-клиента формирует список из истории двухсот прошлых заказов неоптимизированным запросом без пагинации на уровне базы данных, кеш здесь бессилен в принципе — тормозить будет каждый раз заново, для каждого клиента, при каждом заходе.
Изображения и внешние ресурсы: за что вы платите скоростью и приватностью
Четвёртая причина настолько заметна визуально, что удивительно, как часто её игнорируют. Фотографии товаров — это обычно самая тяжёлая часть любой страницы каталога по объёму передаваемых данных, и на магазине с двадцатью тысячами позиций, где на странице категории отображается сорок карточек одновременно, каждая с миниатюрой, суммарный вес изображений одной страницы легко переваливает за пару мегабайт, если никто не следил за форматом и сжатием.
Классическая ошибка — хранить и отдавать фотографии в исходном формате, в котором их прислал поставщик или сфотографировал менеджер, без генерации оптимизированных миниатюр под конкретные места использования. Фото весом три мегабайта, сжатое браузером визуально до размера иконки 200 на 200 пикселей в листинге каталога, — это выброшенные впустую секунды загрузки на каждого посетителя, при том что современные форматы вроде WebP или AVIF при том же визуальном качестве весят в несколько раз меньше классического JPEG. Разумная настройка автоматической генерации миниатюр нужного размера с конвертацией в современные форматы решает львиную долю этой проблемы почти без участия человека — но только если кто-то изначально эту настройку включил и проверил, что она реально применяется ко всем размерам, которые использует тема сайта.
А вот вторая часть этой темы обсуждается на удивление редко, хотя она не менее важна — это внешние ресурсы, подключаемые с чужих доменов. Классический пример — шрифт Google Fonts, подключённый напрямую с серверов Google, или иконки Font Awesome, подключённые с внешней CDN-сети. На первый взгляд это выглядит как разумная экономия — зачем хранить шрифт у себя на сервере, если можно подключить его с быстрой и надёжной внешней сети. На практике происходит следующее: браузер посетителя, помимо соединения с вашим собственным сервером, вынужден устанавливать ещё одно, а иногда и несколько отдельных соединений с чужими доменами — это отдельный DNS-запрос на определение адреса чужого сервера, отдельное TLS-рукопожатие для шифрования соединения, и только после этого начинается собственно загрузка файла шрифта или иконок. Каждый такой внешний домен добавляет десятки, а на нестабильном мобильном соединении — сотни миллисекунд задержки ещё до того, как вообще начнётся передача полезных данных.
Я лично снимал подключение внешнего набора иконок с одного производственного сайта, где раньше библиотека иконок исторически подключалась с чужой CDN-сети буквально на каждой странице сайта — от главной до карточки товара. Простая замена на инлайновые SVG-иконки, встроенные прямо в код страницы вместо отдельного шрифта с чужого сервера, убрала целое внешнее соединение из цепочки загрузки каждой страницы, и время до первой отрисовки контента заметно улучшилось — просто потому что браузеру больше не нужно было ждать ответа от домена, который вообще не имеет отношения к вашему магазину. Причём тут есть и вторая сторона медали, которую владельцы бизнеса часто вообще не рассматривают, — приватность. Каждое обращение браузера посетителя к внешнему домену технически означает, что этот внешний сервис получает информацию о посетителе — его IP-адрес, юзер-агент, факт визита на конкретную страницу вашего сайта. Для магазина, который работает с персональными данными покупателей и обязан соблюдать требования законодательства о защите этих данных, каждый лишний внешний домен — это ещё одна точка, куда утекает информация о посетителях без явного контроля с вашей стороны, и лишний повод для вопросов при проверке.
Ещё одна тонкость с изображениями, о которой стоит сказать отдельно, — это ленивая загрузка и правильный набор размеров под разные экраны. Ленивая загрузка означает, что браузер не тратит время и трафик на изображения, которые находятся далеко за пределами видимой области экрана и, возможно, вообще никогда не будут просмотрены, если покупатель не долистает страницу до конца. Работает это из коробки в современных браузерах через простой атрибут разметки, но требует аккуратности — если ленивую загрузку случайно применить к самому первому, самому важному изображению на странице, тому самому, что определяет время отрисовки крупнейшего элемента, эффект получается обратным: браузер начинает намеренно откладывать показ самого важного, что должно быть видно мгновенно. Я видел такую ошибку не раз — благое намерение ускорить страницу оборачивалось замедлением именно того показателя, который сильнее всего влияет на восприятие скорости покупателем.
Отдельный набор размеров изображений под разные экраны — ещё один инструмент, который часто игнорируют. Нет смысла отправлять на экран смартфона с шириной в четыреста пикселей то же самое изображение в полторы тысячи пикселей шириной, которое показывается на десктопном мониторе — современная разметка позволяет браузеру самому выбирать нужный по размеру вариант файла из подготовленного набора, экономя существенную часть трафика именно на мобильных устройствах, откуда, напомню, приходит большая часть покупателей практически любого магазина.
И последнее в этой теме — сторонние виджеты, не связанные напрямую с самим магазином: онлайн-чат поддержки, виджеты соцсетей, счётчики аналитики нескольких систем одновременно, если кто-то когда-то решил подключить и Яндекс.Метрику, и Google Analytics, и ещё парочку сервисов для А/Б тестирования на всякий случай. Каждый такой виджет — это снова внешний домен, снова DNS-запрос и TLS-рукопожатие, а нередко ещё и заметный по весу JavaScript-код, который должен полностью загрузиться и выполниться прежде, чем страница станет интерактивной. Разумный подход — критически пересмотреть, какие из подключённых сторонних сервисов реально используются, и убрать те, что подключили когда-то из любопытства и с тех пор ни разу не открывали их отчёты.
Разумный подход, который мы реализовали в собственной архитектуре, — полностью избегать подключения внешних CDN на витрине там, где это реально возможно: иконки — инлайновым SVG прямо в разметке, а не отдельным шрифтом с чужого сервера, шрифты — размещёнными на собственном хостинге, а не подгружаемыми с внешнего домена при каждой загрузке страницы. Это не только вопрос скорости, хотя выигрыш в скорости вполне ощутим сам по себе, — это ещё и вопрос предсказуемости: если внешний CDN-сервис вдруг станет недоступен, заблокирован или начнёт отдавать контент с задержкой, ваш магазин не должен зависеть от чужой инфраструктуры, на которую вы не имеете никакого влияния.
Core Web Vitals глазами покупателя, а не Lighthouse
А теперь вернёмся к той самой цифре 34, с которой началась эта статья, и поговорим о том, почему сам по себе балл в Lighthouse или PageSpeed Insights — не самая полезная метрика для владельца бизнеса, хотя и полезный диагностический инструмент для технического специалиста. Балл считается по формуле, которая учитывает десятки факторов с разными весами, включая вещи вроде правильности заголовков кеширования на статичных ресурсах, которые важны для поисковой оптимизации, но никак не влияют на то, купит ли конкретный посетитель товар прямо сейчас.
Гораздо честнее смотреть на скорость сайта глазами реального покупателя, через призму конкретных моментов взаимодействия, которые определяют, оформит он заказ или уйдёт. Первый такой момент — скорость появления главного визуального контента страницы, технически это называется временем отрисовки крупнейшего элемента. Для карточки товара это обычно главное фото или галерея — если оно грузится медленно, покупатель несколько секунд смотрит на пустое место или на плейсхолдер вместо самого товара, и именно в эти секунды принимается решение «останусь смотреть» или «закрою вкладку». Это метрика, за которой реально стоит потерянная или сохранённая продажа, в отличие от многих вспомогательных технических показателей, которые Lighthouse тоже учитывает в своём общем балле.
Второй критичный момент — отклик интерфейса на действия покупателя, современная метрика называется задержкой до следующей отрисовки. Простой пример — покупатель кликает кнопку «добавить в корзину», и что происходит дальше? В хорошо спроектированном магазине кнопка мгновенно даёт визуальный отклик — меняет состояние, показывает индикатор загрузки, и через доли секунды подтверждает добавление. В плохо спроектированном магазине кнопка может ничего не делать одну, две, три секунды — и вот тут начинается классическое поведение расстроенного покупателя: он кликает повторно, думая что первый клик не сработал, товар добавляется в корзину дважды, покупатель видит в мини-корзине два одинаковых товара, пугается, что что-то сломалось, и в лучшем случае тратит время на разбирательство, а в худшем — просто уходит с сайта в раздражении. Это ровно тот сценарий, где отвлечённая техническая метрика напрямую превращается в конкретную упущенную или испорченную покупку.
Третий момент — визуальная стабильность страницы, метрика неожиданного смещения макета. Классический раздражающий сценарий — покупатель начинает читать описание товара, страница дозагружает рекламный баннер или отложенное изображение, и весь текст резко скачет вниз на середине чтения, а покупатель, который уже занёс палец, чтобы нажать кнопку заказа, случайно попадает совсем по другому элементу интерфейса, потому что макет успел съехать. На мобильных устройствах, откуда сейчас идёт большая часть трафика большинства магазинов, эта проблема ощущается ещё острее, потому что экран меньше, а значит, случайное попадание не туда происходит значительно чаще.
Показательный момент — попробовать открыть свой же магазин с принудительным ограничением скорости соединения до уровня среднего мобильного интернета где-нибудь в области, а не в крупном городе с оптоволокном. Разница ощущений разительная: то, что на офисном вайфае разработчика грузится за долю секунды и вообще не вызывает вопросов, на реальном мобильном соединении растягивается на пять-семь секунд, и именно так реальный покупатель воспринимает ваш магазин, если не находится физически в дата-центре рядом с сервером. Инструменты разработчика в любом современном браузере позволяют искусственно ограничить скорость соединения буквально парой кликов, и я настоятельно рекомендую владельцам магазинов хотя бы раз проделать это самостоятельно, прежде чем доверять исключительно автоматическим отчётам о производительности.
Именно поэтому, когда мне присылают скриншот с низким баллом PageSpeed Insights и просят «просто починить», я всегда прошу подождать с конкретными техническими действиями и сначала пройти путь реального покупателя вручную — открыть сайт с мобильного телефона на не самом быстром соединении, пройти от главной страницы до оформления заказа, замечая каждый момент, где приходится ждать, где что-то дёргается на экране, где кнопка не реагирует сразу. Это даёт куда более точную картину реальных потерь, чем абстрактная цифра из автоматического отчёта, и позволяет расставить приоритеты по тому, что действительно стоит между посетителем и оформленным заказом, а не по тому, что проще всего исправить для красивой цифры в отчёте.
Возвращаясь к магазину строительных материалов, с которого началась эта история — мы прошли именно этим путём. Оказалось, что балл 34 в первую очередь складывался из связки всех четырёх причин сразу: разросшаяся база без нужных вспомогательных таблиц, двадцать три активных плагина, из которых половина дублировала функциональность друг друга, кеш, который не покрывал страницу с активным фильтром, и внешний шрифт с чужого CDN, подключаемый на каждой странице. Мы не гнались за красивой цифрой в отчёте как за самоцелью — мы чинили по порядку, начиная с того, что реально стояло между покупателем и оформленным заказом, а не с того, что было проще всего исправить за один вечер ради красивого скриншота для следующего отчёта владельцу. Итоговый балл в Lighthouse действительно вырос, но что важнее — вырос не он сам по себе, а количество людей, которые долистывали до кнопки оформления заказа и реально её нажимали.
Если ваш магазин на большом каталоге ведёт себя похожим образом — тормозит, но никто толком не может объяснить, из-за чего именно, — начните не с покупки очередного плагина ускорения, а с честного разбора всех четырёх причин по отдельности. Иногда выясняется, что чинить нужно не то, о чём все сразу подумали, а иногда — что чинить нужно сразу всё понемногу, потому что причины давно сложились в одну общую картину, и вытащить из неё только одну нитку, не тронув остальные, попросту не получится.
Мы в COS WP Woo с самого начала проектировали архитектуру плагина с оглядкой именно на эти четыре фронта одновременно — вынесенный на отдельный движок поиск, чтобы не упираться в постметы, единая кодовая база модулей вместо конкурирующих друг с другом плагинов, дист-сборка витринных ассетов с минификацией из коробки, и полный отказ от внешних CDN на витрине. Ни один из этих пунктов сам по себе не решает проблему медленного магазина целиком — но вместе они снимают три с половиной причины торможения из четырёх ещё до того, как вы вообще начнёте разбираться, что не так с конкретно вашим сайтом. Четвёртая, работа с непосредственно самим хостингом и его настройками PHP, всё равно остаётся на стороне владельца магазина или его подрядчика — и здесь никакой плагин, как бы хорошо он ни был спроектирован, не заменит банального разговора с провайдером хостинга о том, какая версия PHP используется и включён ли кеш кода на сервере.
