<? phpukraine СПІВБЕСІДИ
Пошук по платформі
LARAVEL · SENIOR

Як знайти, чому Laravel-сторінка повільна, і що оптимізувати першим?

Спершу розкласти час: браузерний waterfall відділяє TTFB від фронтенду, а далі Debugbar/Clockwork/Telescope локально й Blackfire або Pulse у проді показують, куди йдуть мілісекунди всередині PHP. Оптимізують за спаданням внеску: кількість запитів (N+1) → важкі запити з `EXPLAIN` → синхронні зовнішні виклики в чергу → гігієна деплою (`opcache`, `php artisan optimize`, `composer -o`), і лише потім кеш і Octane.

Сторінка відкривається 4 секунди. З чого почнете і чим будете міряти?
Профайлер показує, що 70% часу — в `PDO::execute`. Що це означає і які наступні кроки?
Debugbar каже 1.8 с, а користувач скаржиться на 8 с. Хто з них бреше?
Ми зробили `php artisan optimize` і винесли розсилку в чергу — сторінка все одно повільна. Чому такий порядок дій був неправильний?
профілювання Telescope Debugbar Clockwork Blackfire Xdebug opcache черги Pulse

Перш ніж називати інструменти, треба відповісти на питання «повільно де». Час до появи сторінки складається щонайменше з пʼяти шматків: мережа й TLS, час до першого байта (тобто робота PHP), завантаження статики, виконання JavaScript у браузері й запити, які цей JavaScript робить далі. Перший крок — вкладка Network у DevTools: якщо TTFB документа 180 мс, а сторінка «думає» 6 секунд, оптимізовувати Laravel безглуздо, і чесна відповідь на співбесіді починається саме з цього розмежування. Якщо ж повільний TTFB, час усередині PHP розкладається на завантаження фреймворку (автозавантаження, реєстрація провайдерів, читання конфігу), запити до БД, синхронні виклики зовнішніх сервісів, рендер шаблонів і серіалізацію. Дешевий спосіб зробити цю декомпозицію видимою навіть без профайлера — заголовок Server-Timing із кількома мітками або власне логування, як у прикладі коду: пара чисел total_ms, db_ms, queries майже завжди одразу каже, куди дивитись.

Інструменти відрізняються не «зручністю», а тим, що вони бачать і скільки коштують. Laravel Debugbar (barryvdh/laravel-debugbar) — це панель, що вбудовується в HTML-відповідь: він показує список запитів із часом і дублікатами, таймлайн, вʼюхи, події й памʼять. Це dev-інструмент: для JSON-API він не працює звичним чином, у проді його не вмикають, а його власні накладні витрати помітні, тож абсолютні числа з нього — орієнтир, а не вимір. Clockwork (itsgoingd/clockwork) збирає ті самі дані, але віддає їх не в HTML, а окремим ендпоїнтом і заголовками, тому працює для API, artisan-команд і job у чергах — там, де Debugbar сліпий. Telescope — це вже не панель, а журнал: watchers пишуть запити, HTTP-запити, кеш, пошту, job і винятки в таблиці telescope_entries, і саме тому він однаково корисний для розслідування «що сталося о 14:07» і небезпечний як постійно ввімкнена штука на продакшені. Laravel Pulse — протилежний за дизайном інструмент: він семплить і агрегує, показуючи повільні запити, повільні маршрути, повільні job і споживачів ресурсів, тобто відповідає на питання «що болить загалом», а не «що сталося в цьому конкретному запиті».

Коли треба спуститися нижче рівня «який запит повільний» і зрозуміти, який саме PHP-код зʼїдає час, беруть справжній профайлер. Xdebug 3 із xdebug.mode=profile і xdebug.start_with_request=trigger (запуск через XDEBUG_TRIGGER=PROFILE, щоб не профілювати все підряд) пише cachegrind-файли в xdebug.output_dir, які читають у KCachegrind, Webgrind або PhpStorm; звідти видно дерево викликів, кількість викликів і власний час функцій. Ціна — сповільнення в рази й спотворення пропорцій: очікування на БД і мережу лишається таким, як було, а PHP-частина роздувається, тому за профілем Xdebug легко «оптимізувати» те, що в проді не важить нічого. Blackfire і Tideways зроблені саме для того, щоб цього уникнути: вони інструментують вибірково, дають розбивку по wall time, CPU, I/O, памʼяті й кількості SQL-запитів та HTTP-викликів, працюють на продакшені й уміють blackfire run для CLI та assertions у CI («ця сторінка не має робити більше N запитів»). Окремо варто знати про семплінг-профайлери (php-spx, excimer) — вони дешеві настільки, що їх тримають увімкненими постійно. І базова гігієна вимірювання: якщо у вас локально завантажений Xdebug, увімкнений Debugbar і APP_DEBUG=true з логуванням запитів — ваші числа не мають нічого спільного з продом.

Тепер про порядок оптимізацій, бо саме за нього ставлять плюс. Оптимізують за спаданням внеску в загальний час, і на реальних Laravel-проєктах рейтинг майже завжди такий. Перше — кількість запитів до БД: сторінка на 200–2000 запитів — це N+1, і тут виграш вимірюється не відсотками, а порядками (механіка й лікування — в окремій картці про N+1). Друге — окремі важкі запити: коли запитів десяток, але один іде 900 мс, далі говорить EXPLAIN/EXPLAIN ANALYZE, індекси, порядок колонок у складеному індексі, глибокий OFFSET і COUNT(*) заради пагінації. Третє — синхронні зовнішні виклики: відправка листа, звернення до платіжки, генерація PDF чи ресайз усередині HTTP-запиту роблять час відповіді заручником чужого сервера; це виносять у чергу (ShouldQueue), а дрібне — у dispatch(...)->afterResponse(). Четверте — гігієна деплою, яку часто пропускають, хоч вона одноразова: composer install --no-dev --optimize-autoloader (за можливості --classmap-authoritative), php artisan optimize (кеш конфігу, роутів, подій і вʼюх), APP_DEBUG=false, кеш і сесії не на файлах, а в Redis. Останнє звучить дрібно, але файловий драйвер сесій блокує файл на час запиту, і паралельні запити одного користувача шикуються в чергу — типовий «повільний дашборд», де кожен віджет чекає на попередній.

Окремо — OPcache, бо це єдина інфраструктурна річ, яку варто вміти перевіряти руками. Без нього PHP компілює тисячі файлів на кожен запит; з ним — читає готовий байткод із розділюваної памʼяті. Але «увімкнений» не означає «налаштований»: дефолтний opcache.max_accelerated_files=10000 для Laravel із залежностями замалий, і переповнений кеш дає плаваючу деградацію, яку в профайлері видно як розмазаний час компіляції. Дивляться на opcache_get_status(): num_cached_keys проти max_cached_keys, wasted_percentage, hit rate. У проді opcache.validate_timestamps=0 прибирає перевірку часу зміни файлів, але деплой зобовʼязаний перезапускати php-fpm; opcache.jit для типового веб-навантаження дає близько нуля, бо код чекає на БД, а не рахує; preload має практичний сенс разом із FrankenPHP або Octane. І тут же межа: php artisan config:cache після себе вимагає, щоб env() не викликався ніде, крім config/, а route:cache не переживе замикань у роутах — обидві помилки зазвичай виявляються на проді й виглядають як зламана функціональність, а не як проблема продуктивності.

І про компроміси, які варто назвати вголос. Кеш (Cache::remember, кеш фрагментів, HTTP-кеш) — потужний, але це не діагностика: він ховає вартість, додає інвалідацію й перетворює деградацію на рідкісну та непередбачувану (перший запит після скидання, cache stampede на популярному ключі). Octane прибирає bootstrap із кожного запиту, але приносить свій клас багів зі станом між запитами, і братися за нього до того, як прибрано N+1, — це прискорювати те, що займає 5% часу. Черга не робить нічого швидшим — вона переносить роботу й додає вам стан «в обробці», ідемпотентність і моніторинг воркерів. Нарешті, метрика: орієнтуватися треба на p95/p99, а не на середнє, і міряти на реальному обсязі даних, бо сторінка, що літає на 100 рядках, помирає на 100 тисячах через відсутній індекс. Дисципліна проста й саме вона відрізняє senior-відповідь: одна зміна — один повторний вимір, інакше через тиждень ніхто не скаже, що саме допомогло, і чи допомогло взагалі.

final class PerformanceServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        // Ліниве завантаження звʼязку — головне джерело N+1: локально падаємо одразу,
        // у проді не ламаємо сторінку, а пишемо модель і звʼязок у лог.
        Model::preventLazyLoading(! $this->app->isProduction());
        Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation): void {
            Log::channel('perf')->warning('lazy load', [
                'model' => $model::class,
                'relation' => $relation,
            ]);
        });

        $queries = 0;
        $queryMs = 0.0;

        DB::listen(function (QueryExecuted $query) use (&$queries, &$queryMs): void {
            $queries++;
            $queryMs += $query->time; // мілісекунди, вимір самого драйвера

            if ($query->time >= 200) {
                // $query->sql — з плейсхолдерами, значення не потрапляють у лог
                Log::channel('perf')->warning('slow query', ['ms' => $query->time, 'sql' => $query->sql]);
            }
        });

        // terminating() виконується вже після відправлення відповіді — нічого не сповільнює.
        $this->app->terminating(function () use (&$queries, &$queryMs): void {
            // Під Octane константа ставиться раз на воркер: там потрібна власна мітка часу.
            $totalMs = (microtime(true) - LARAVEL_START) * 1000;

            if ($totalMs < 1000) {
                return;
            }

            Log::channel('perf')->warning('slow request', [
                'total_ms' => round($totalMs),
                'db_ms' => round($queryMs),   // db_ms ≈ total_ms -> винна база, а не PHP
                'queries' => $queries,        // 200 запитів по 2 мс -> це N+1, а не «повільна БД»
                'peak_mb' => round(memory_get_peak_usage(true) / 1048576, 1),
            ]);
        });
    }
}
Що починають з вимірювання й декомпозиції часу, а не з гіпотези: DevTools waterfall або `Server-Timing` спершу відповідають на питання «повільно на сервері чи в браузері».
Що кандидат розрізняє інструменти за ціною й місцем: Debugbar — лише dev і лише HTML-відповіді, Clockwork — ще й API/CLI/черги, Telescope — запис у БД з watchers і `telescope:prune`, Xdebug-профайлер — офлайн-аналіз із кратним сповільненням, Blackfire/Tideways/Pulse — продакшен.
Що Xdebug у режимі `profile` спотворює пропорції (роздуває виклики функцій відносно I/O) і що сам факт завантаженого розширення в dev робить локальні числа непорівнянними з продом.
Що оптимізують за внеском у загальний час, а не за красою: 300 запитів по 1 мс і один запит на 900 мс лікуються по-різному, і `EXPLAIN` відрізняє одне від одного.
Що `php artisan optimize`, `composer install --no-dev --optimize-autoloader` й правильно налаштований OPcache — це дешева одноразова гігієна на деплої, а не оптимізація алгоритму, і вона не рятує від N+1.
Що в чергу виносять роботу, результат якої не потрібен у цій відповіді (лист, PDF, вебхук, ресайз), і що черга нічого не пришвидшує — вона переносить час на воркер.
Що дивляться на p95/p99 і на медіану, а не на середнє, і міряють на прогрітому кеші й реальних обсягах даних.
Починати з `Cache::remember()` навколо повільного місця: кеш ховає симптом, а перший запит після інвалідації, деплой і кожен новий фільтр повертають ті самі 4 секунди.
Тримати Xdebug увімкненим локально (`xdebug.mode=debug,develop`) і робити з локальних мілісекунд висновки про продакшен — накладні витрати там кратні.
Вважати профайл із `xdebug.mode=profile` картою реального часу: cachegrind добре показує, що викликається мільйон разів, і погано — скільки насправді чекали на мережу й диск.
Ставити `config:cache` і залишати `env('SOMETHING')` у сервіс-провайдері чи моделі: після кешування конфігу `env()` поза файлами `config/` повертає `null`, і це зазвичай виглядає як «зламався інший модуль», а не як помилка деплою.
Робити `route:cache` при закритих у роутах замиканнях і не розуміти `LogicException: Unable to prepare route ... Uses Closure`.
Оптимізувати PHP, коли TTFB — 200 мс, а 6 секунд дає 4 МБ JS і десяток запитів до сторонніх скриптів у браузері.
Тримати Telescope увімкненим у проді з усіма watchers і без `telescope:prune`: кожен запит починає писати десятки рядків у ту саму базу, яку ви й намагаєтесь розвантажити.
Міряти середній час відповіді: одна сторінка адмінки на 12 секунд у середньому по 200 тис. запитів не видно взагалі.
Змінювати п'ять речей одночасно, а потім не мати змоги сказати, що саме допомогло.
ПОРАДА

Скажіть, що у вас є фіксована послідовність: розкласти час (браузер → TTFB → PHP → БД → зовнішні виклики), знайти найбільший доданок, змінити одну річ, повторно виміряти. І одразу назвіть, що дає найбільше на реальних проєктах: кількість запитів, а не їхня швидкість. Фраза, яку запамʼятовують: «кеш і Octane — це не діагностика, а анестезія; спершу треба знати, що болить».

оновлено 4 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

`php artisan optimize` кешує конфіг, роути, події та вʼюхи; після `config:cache` файл `.env` більше не читається під час запиту, тому `env()` поза `config/` повертає `null` — це найпоширеніший наслідок цієї команди. Xdebug у режимі `profile` пише cachegrind-файл на кожен виклик функції й сповільнює виконання кратно, тому в проді його не тримають, а профілі читають з поправкою на те, що час усередині PHP роздутий відносно очікування на БД і мережу. Debugbar живе лише в dev, працює тільки з HTML-відповідями й сам додає помітні накладні витрати, тож його число не є числом продакшену.