16 августа 2026 года наш собственный сайт отвечал за 3,05 секунды. Соседние проекты на том же сервере отвечали за 0,2–0,3 секунды. То есть хостинг был ни при чём: одна машина, одна сеть, разница в десять раз.
Причину мы нашли не в коде страницы, а в блоке, который стоит на каждой странице и на который никто не смотрит. Ниже разбор того случая и ещё трёх похожих, которые мы исправили до 5 сентября, с цифрами до и после. В конце покажем, как за 10–15 минут проверить то же самое на своём сайте.
Меню в шапке, которое весило 4,3 МБ
Мы замеряли не страницу целиком, а каждый источник данных отдельно. Один запрос занимал 1 791 мс из 2,4 секунды всего рендера. Это был запрос для мега-меню в шапке, то есть он выполнялся на любой странице сайта: на главной, в статье, на странице услуги.
Меню нужны три коротких поля на пункт: название, заголовок и краткое описание. Запрос брал 185 строк вместе со столбцом переводов. А в том столбце лежали не только названия, но и полный текст каждой страницы на трёх языках: 23 КБ на строку, 4,3 МБ на все. Чтобы прочитать несколько сотен байтов, база каждый раз отдавала мегабайты.
Лучшая деталь: над этим запросом стоял комментарий «лёгкий срез — без тяжёлого content JSON». Буквально комментарий не врал. Поле с текстом страницы действительно не выбиралось отдельно, но оно лежало внутри поля перевода, которое выбиралось целиком.
Почему этого никто не заметил
Потому что в коде страницы этого запроса нет. Открываешь шаблон статьи, и там только запросы самой статьи. Меню живёт в шапке, шапка в общем макете, и когда ищут, почему медленная статья, смотрят в статью.
К тому же запрос не был ошибочным. Он возвращал правильные данные, меню показывало правильные пункты. Медленный запрос, который работает правильно, сам о себе не сообщает.
Исправили в два шага. Сначала нужные поля стали забираться отдельным запросом по конкретным путям внутри JSON, а не целым столбцом. Сам этот запрос ускорился с 1 891 до 107 мс. Потом готовое дерево меню положили в кеш данных на час, со сбросом, когда в админке публикуют страницу. Второе важнее: с кешем этот запрос на большинстве показов не выполняется вообще.
Тот же узор ещё трижды
После меню мы начали искать именно такие места: невидимые со страницы и одинаковые для каждого показа. Нашлось ещё три.
Настройки сайта, 23 августа. Каждый рендер любой публичной страницы читал таблицу настроек 7–8 раз: теги аналитики (дважды, в двух разных местах), строки для структурированных данных, флажок индексации, список включённых языков. Эти значения меняют из админки редко, а читались они на каждый запрос. В тот день мы замерили 0,46 с на простой странице и 0,72 с на сложной, при том что статический файл с того же сервера приходил за 0,30 с. Чтение обернули в кеш со сроком жизни 60 секунд и сбросом при сохранении. Почему ещё и минута, а не только сброс: писать в ту таблицу умеют как минимум двенадцать мест в коде, и пропустить одно очень легко. Кеш без сброса хуже отсутствия кеша, потому что правка из админки просто не появляется.
Страницы услуг, 5 сентября. Страницы сети услуг отвечали за 0,45–0,5 с, хабы разделов за 0,17 с, статьи блога за 0,36–0,43 с. Виноват блок «смежные услуги» внизу страницы: он выбирал до 60 соседей вместе с полными переводами. Выходило 1,1–1,3 МБ JSON против 65 КБ всех остальных данных страницы, хотя ссылкам нужны только название и заголовок на одном языке. Та же ошибка, что и в меню, только в другом месте. Заодно выяснилось, что строка самой страницы читалась дважды: для метаданных и для тела.
Автоматические ссылки в статьях, 5 сентября. Здесь виновата уже не база, а процессор. Профиль показал, что самая большая единичная статья расходов на странице статьи — подстановка внутренних ссылок. На каждый из примерно 2 000 ключей собиралось регулярное выражение и прогонялось по всему тексту. Это 35 мс процессора на показ, а на общем сервере с load average 5–9 эти миллисекунды растягивались в более чем 0,1 с ответа. Заменили на поиск подстроки с проверкой границ слова. Старую и новую версию сравнили на 679 текстах (все опубликованные статьи и вступления страниц на трёх языках): результат побайтово одинаковый. Самая тяжёлая статья обрабатывается за 5,6 мс вместо 18,9, весь набор за 464 мс вместо 1 451.
Честная оговорка: цифры «до» в этих случаях сняты в разные дни и разными способами. Это не одна кривая, а четыре отдельные истории, и складывать их в одно «ускорили в N раз» было бы неправдой.
Сколько сейчас и при чём здесь сеть
16 сентября 2026 года около 20:40 по Киеву мы повторили замер. С самого сервера до публичного адреса сайта, по 5 запросов на адрес, медиана времени до первого байта:
Load average сервера в момент замера был 6,1, то есть это цифры под обычной для него нагрузкой, а не на пустой машине.
Теперь то, что видит человек в Украине. С нашего ПК, по 6 запросов:
Статический файл сайт не рендерит вообще, он просто лежит на диске. И всё равно 0,19–0,22 секунды, из них около 0,1 с только на установку соединения. Это пол: ниже него оптимизация кода не опустит, потому что это расстояние и сеть, а не программа.
Как проверить свой сайт за 10–15 минут
1. Снимите пол. Возьмите любой статический файл своего сайта (логотип, CSS) и страницы, которые вас беспокоят:
for u in /logo.svg / /katalog/; do
printf "%s ::" "$u"
for i in 1 2 3 4 5 6; do
curl -s -o /dev/null -w " %{time_starttransfer}" "https://ваш-сайт.com.ua$u"
done; echo
done
От времени страницы отнимите время статического файла. Остаток — это работа вашего сервера и вашего кода. Если остаток несколько десятков миллисекунд, в коде искать нечего, вопрос в сети или в расположении сервера. Если секунда, читайте дальше.
2. Сравните разные типы страниц. Главная, категория, карточка товара, статья, «Контакты». Если медленные все одинаково, даже самые простые, причина почти наверняка в том, что есть на каждой странице: шапка, меню, футер, настройки. Если медленная только категория, смотрите её собственные запросы.
3. Найдите тяжёлый запрос. Включите журнал медленных запросов и откройте страницу несколько раз. Для MySQL:
SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 0.1;
Для PostgreSQL с расширением pg_stat_statements:
SELECT round(mean_exec_time) AS ms, calls, left(query, 120)
FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10;
Запрос с большим calls и заметным временем — главный подозреваемый: он выполняется на каждый показ.
4. Посмотрите, сколько весят столбцы, которые он берёт. В нашем случае время уходило не на поиск, а на перебрасывание данных. В PostgreSQL:
SELECT avg(pg_column_size(название_столбца)) FROM название_таблицы;
Если запрос для меню берёт столбец на десятки килобайтов на строку, а показывает из него одно слово, вы нашли то же самое, что и мы.
Про OpenCart и подобные движки скажем осторожно, потому что отдельно мы это не мерили: там роль нашего мега-меню обычно играют модули в шапке и футере, которые выполняются на каждой странице. Проверять их стоит первыми, но это аналогия, а не замеренный факт.
Что это не исправило
Самое важное ограничение видно в данных реальных посетителей. За 28 дней до 7 сентября 2026 года 75-й перцентиль времени до первого байта на мобильных был 893 мс (109 замеров), на десктопах 654 мс (124 замера). Учитываются только посетители, которые согласились на аналитику, а окно частично захватывает период до исправлений 5 сентября. Но разрыв между 0,2 с на сервере и почти 0,9 с на телефоне — это преимущественно мобильная сеть, и кодом сайта его не убрать. Помочь могла бы только отдача готовых страниц из кеша, без рендера. Украинские страницы у нас до сих пор рендерятся на каждый запрос: в их адресах нет языкового префикса, путь переписывается, а переписанный запрос идёт мимо кеша страниц.
У кеширования есть и своя цена. Меню обновляется сразу после публикации в админке, а вот настройки сайта могут отставать до минуты. Мы на это сознательно согласились.
Вывод, который переносим на любой сайт: прежде чем винить хостинг или CMS, снимите пол статическим файлом, а потом ищите один тяжёлый запрос в блоке, который стоит на каждой странице. Если заниматься этим самим нет времени, мы делаем аудит скорости сайта и ускорение с такими же замерами до и после. Какие числа скорости вообще измерять, разобрано отдельно, а про вес страницы и сжатие есть история про 170 КБ вместо 76.