Автозагрузка опций WordPress: что ломается под нагрузкой
Один SELECT и один unserialize на каждом запросе к сайту — пока автозагрузка весит 50 КБ, это незаметно. Разбираем, что происходит с wp_options на двух мегабайтах, частых записях и Redis под сотнями одновременных запросов.

Пока в автозагрузке лежит 50 КБ, вы о ней не думаете. Когда там два мегабайта, WordPress на каждом запросе — включая admin-ajax.php, REST и cron — вытаскивает весь этот массив из базы или из Redis, разворачивает его в память PHP и держит до конца запроса. Это происходит до того, как сработает первый хук темы. На пяти посетителях цена незаметна. На трёхстах одновременных запросах она умножается на каждый воркер PHP-FPM и превращается в заметный кусок CPU, памяти и сетевого трафика до кеша. Ниже — механизм, способ измерить объём у себя и граничные случаи, где очевидное лечение (выключить автозагрузку) делает хуже.
Что делает ядро на каждом запросе
При загрузке WordPress вызывает wp_load_alloptions(). Логика простая:
- Проверяется кеш объектов: ключ
alloptionsв группеoptions. - При промахе выполняется один запрос, который выбирает все строки
wp_options, помеченные как автозагружаемые, и складывает их в один PHP-массив. - Массив кладётся в кеш объектов целиком, одним ключом.
Дальше любой get_option() для автозагружаемой опции — это обращение к уже готовому массиву в памяти, без запроса. Опции вне автозагрузки читаются по одной: get_option() смотрит кеш по имени опции, при промахе делает отдельный SELECT. Имена опций, которых в базе нет вообще, копятся в отдельном кеш-ключе notoptions, чтобы не ходить в базу за одним и тем же отсутствующим значением дважды.
Что изменилось в WordPress 6.6
До 6.6 колонка autoload принимала yes и no. В 6.6 набор значений расширили: появились on, off и значения с префиксом auto, которые означают «ядро решает само». Решает по размеру: если значение опции больше порога (по умолчанию порядка 150 КБ, порог меняется фильтром wp_max_autoloaded_option_size), новая опция автозагружаться не будет. Старые строки со значениями yes/no при обновлении не переписывались массово и продолжают работать, поэтому в реальной базе вы увидите смесь. Это важно для диагностических запросов: фильтр только по autoload = 'yes' на свежем сайте покажет неполную картину.
Автоопределение закрывает случай «плагин положил в опцию мегабайтный JSON», но не случай «четыреста опций по 3 КБ». Сумма порогом не контролируется.
Почему объём бьёт именно под нагрузкой
Стоимость автозагрузки — не один раз за деплой, а один раз за запрос. Дальше всё зависит от того, есть ли persistent object cache.
Без внешнего кеша объектов. Кеш живёт только внутри одного запроса. Значит, на каждый запрос к PHP — один SELECT, который тянет весь объём автозагрузки по сети от MySQL. Два мегабайта на 200 запросов в секунду — это сотни мегабайт в секунду трафика между PHP и базой на данных, которые не менялись неделю.
С Redis или Memcached. SELECT уходит, но появляется другая цена. Весь массив лежит одним ключом, поэтому читается он тоже целиком: сеть до Redis, затем unserialize полного массива в каждом воркере на каждом запросе. CPU на десериализацию и память на массив умножаются на параллельность, а не на число сайтов.
Записи — самое неприятное. Когда update_option() меняет опцию, которая входит в автозагрузку, ядро не правит один элемент в кеше: оно подставляет новое значение в массив и записывает в кеш весь alloptions заново. Одна обновлённая строка на 40 байт — это запись двух мегабайт в Redis. Если плагин пишет счётчик, метку времени или «последнюю синхронизацию» в автозагружаемую опцию на каждом хите, вы получаете поток полных перезаписей большого ключа и гонку между параллельными запросами: два воркера прочитали массив, оба записали свою версию, изменение одного из них в кеше потерялось. В базе данные целы, кеш разъехался до следующего сброса.
Автозагрузка — не «ещё немного данных в базе». Это фиксированный налог на каждый запрос, который вы платите объёмом, умноженным на трафик и на число воркеров.
Как измерить у себя
Сначала объём и состав. Запрос ниже учитывает оба набора значений autoload; префикс таблицы подставьте свой.
-- Суммарный вес автозагрузки
SELECT COUNT(*) AS options_count,
ROUND(SUM(LENGTH(option_value)) / 1024, 1) AS total_kb
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on');
-- Двадцать самых тяжёлых
SELECT option_name,
ROUND(LENGTH(option_value) / 1024, 1) AS size_kb,
autoload
FROM wp_options
WHERE autoload IN ('yes', 'on', 'auto', 'auto-on')
ORDER BY LENGTH(option_value) DESC
LIMIT 20;
Затем цена десериализации — её база не покажет. Скрипт для WordPress 6.6 (на более старых версиях тоже работает, wp_load_alloptions() существует давно), запускать через WP-CLI: wp eval-file alloptions-check.php.
<?php
// Замер веса и стоимости десериализации автозагрузки.
// Запуск: wp eval-file alloptions-check.php
$alloptions = wp_load_alloptions();
$serialized = serialize( $alloptions );
$iterations = 100;
$started_at = microtime( true );
for ( $i = 0; $i < $iterations; $i++ ) {
unserialize( $serialized );
}
$average_ms = ( microtime( true ) - $started_at ) * 1000 / $iterations;
printf(
"Опций в автозагрузке: %d\nРазмер массива: %.1f КБ\nunserialize в среднем: %.3f мс\n",
count( $alloptions ),
strlen( $serialized ) / 1024,
$average_ms
);
Если WP-CLI нет и файл приходится класть в корень сайта, удаляйте его сразу после запуска: диагностический скрипт в вебруте — открытый эндпоинт.
| Показатель | Чем снять | О чём говорит |
|---|---|---|
| Суммарный вес автозагрузки | SQL выше; проверка в Site Health (WordPress 6.6+) | Ядро начинает предупреждать при объёме порядка 800 КБ — уточните актуальный порог для своей версии |
| Число автозагружаемых опций | Тот же SQL, COUNT(*) | Несколько тысяч строк дают вес даже при мелких значениях и замедляют unserialize |
Время unserialize | Скрипт выше | Это нижняя граница вклада в TTFB, которую не уберёт никакой page cache для авторизованных |
| Записи в автозагружаемые опции | Query Monitor на типовой странице: UPDATE по wp_options | Любая запись = полная перезапись ключа alloptions в Redis |
| Трафик до Redis или MySQL | Метрики сервера, INFO stats в Redis | Рост сетевого трафика пропорционально RPS при неизменных данных |
Кто наполняет автозагрузку
Четыре источника покрывают почти все случаи:
- Транзиенты без срока жизни.
set_transient()с указанным временем создаёт опцию вне автозагрузки, а бессрочный транзиент сохраняется как автозагружаемый. Плагины кладут туда разобранные фиды, ответы API и сериализованные деревья категорий. - Кеш, которому не нашлось места. Плагин сохраняет готовый HTML, карту ссылок или настройки конструктора в одну опцию и не передаёт флаг автозагрузки. Исторически по умолчанию опция попадала именно в автозагрузку.
- Логи и счётчики. «Последняя проверка лицензии», «статистика показов», журнал ошибок формы. Горячая запись на каждом хите в большой общий ключ.
- Остатки удалённых плагинов. Удаление через админку не всегда вызывает чистку опций. Строки живут годами и грузятся на каждом запросе.
Как менять autoload, не сломав кеш
В WordPress 6.6+ для этого есть отдельные функции, и это предпочтительный путь:
// WordPress 6.6+: снять автозагрузку с одной опции
wp_set_option_autoload( 'my_plugin_catalog_cache', false );
// Пачкой, одним обращением к базе
wp_set_options_autoload(
array( 'my_plugin_catalog_cache', 'my_plugin_import_log' ),
false
);
Подпись и доступность функций в вашей версии сверяйте в справочнике разработчика: поведение update_option() в части флага автозагрузки между версиями менялось, и полагаться на третий аргумент для уже существующей опции — ненадёжно.
На версиях до 6.6 остаётся прямой UPDATE колонки autoload в wp_options. И тут главная подножка: база изменилась, а устаревший массив alloptions продолжает жить в Redis. После ручного UPDATE обязательно сбросьте кеш объектов (wp cache flush или удаление ключа alloptions в группе options), иначе вы час будете смотреть на старое поведение и думать, что ничего не помогло.
Где лечение ломается
«Выключить автозагрузку у всего тяжёлого» — не универсальный ответ.
Горячая опция вне автозагрузки стоит дороже
Опция, которой нет в alloptions, читается отдельным запросом при первом обращении. Без persistent object cache это +1 SELECT на каждый запрос страницы. Снимите автозагрузку с сорока мелких опций, которые нужны на каждой странице, — и вместо одного большого SELECT получите сорок мелких, каждый со своим round-trip. По объёму данных выигрыш, по латентности проигрыш.
Критерий решения
Смотрите не на размер, а на произведение размера и частоты чтения. В автозагрузке должно остаться то, что читается почти на каждом запросе и весит мало. Снимать стоит с того, что весит много и читается редко: настройки, нужные только в админке, кеши конкретного плагина, журналы. Опция на 200 КБ, которую читает одна страница настроек, не должна грузиться на фронтенде никогда.
notoptions тоже растёт
Массовый перевод опций из автозагрузки означает, что часть кода будет регулярно спрашивать значения, которых в кеше нет. Ключ notoptions и россыпь одиночных ключей опций в Redis — это тоже память и обращения, просто менее заметные, чем один большой alloptions.
Один ключ не делится средствами ядра
Разбить alloptions на части ядро не умеет. Некоторые коммерческие drop-in'ы кеша объектов предлагают особую обработку этого ключа — есть ли она в вашей сборке, смотрите в документации конкретного drop-in'а, а не в документации WordPress.
Мультисайт
Сетевые опции живут в wp_sitemeta, колонки autoload там нет, и описанный механизм к ним напрямую не относится. Но у каждого сайта сети — свой alloptions, и на общем Redis их объёмы складываются в память инстанса. Это отдельная тема, здесь её не разворачиваем.
Что делать дальше
Порядок действий, который даёт результат без экспериментов на проде:
- Снимите два числа на копии базы: вес автозагрузки и время
unserialize. Это точка отсчёта. - Разберите топ-20 тяжёлых опций по владельцам. Для каждой ответьте: кто её читает и на каких страницах.
- Переведите вне автозагрузки только те, что не нужны на фронтенде. После смены — сброс кеша объектов и повторный замер.
- Найдите записи в автозагружаемые опции на обычном хите страницы через Query Monitor. Горячие счётчики переносите в свою таблицу или во внешнее хранилище, а не в опции.
- Включите ежедневную чистку просроченных транзиентов (в ядре за это отвечает событие cron, проверьте, что WP-Cron у вас действительно отрабатывает) и убедитесь, что плагины ставят транзиентам срок жизни.
- Поставьте мониторинг: периодический запрос суммарного веса автозагрузки с порогом алерта. Проверка в Site Health покажет проблему только тому, кто зайдёт в админку.
Главный показатель после правок — не вес сам по себе, а время до первого байта на авторизованном запросе, где page cache не спасает. Если автозагрузка была узким местом, разница видна там в первую очередь.
Автозагрузка (autoload) — флаг строки в таблице wp_options. Все помеченные им опции WordPress читает одним запросом в начале каждого запроса, независимо от того, понадобятся они странице или нет.
Persistent object cache — внешнее хранилище кеша объектов (Redis, Memcached), которое живёт между запросами. Без него кеш объектов WordPress существует только внутри одного запроса и умирает вместе с ним.
Сериализация и unserialize — превращение PHP-массива в строку для хранения и обратно. Операция стоит процессорного времени пропорционально размеру данных и выполняется на каждом запросе заново.
Аналитик звонков: Разбор записи звонка по этапам сделки: где сорвалось, что упущено, три исправляемые рекомендации с цитатами.
Спросить за 90 ₽Источники: wp_load_alloptions() — WordPress Developer Resources, wp_set_option_autoload() — WordPress Developer Resources
Частые вопросы
Какой вес автозагрузки считать нормальным?
У меня WordPress 6.5, функций wp_set_option_autoload там нет. Что делать?
Снял автозагрузку с тяжёлых опций, а время ответа не изменилось. Почему?
Можно ли просто удалить большие опции от плагинов, которых уже нет?
Какие подводные камни ждут в проде при массовом переводе опций вне автозагрузки?
Решает ли проблему установка Redis?
Почему в колонке autoload у меня одновременно yes, no, on и off?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.