Тема
Производительность Laravel — лучшие практики
Application-слой. Дополняет
../code-quality.md(раздел производительности) и../migrations.md(индексы). Инфраструктурный слой (тюнинг PostgreSQL/PHP-FPM/Redis/NGINX/Docker, бюджет памяти, уровни кеширования) — в infrastructure-performance.md.
Иерархия оптимизации (сверху — наибольший эффект)
- Устранить N+1 (
->with(),preventLazyLoadingв dev) — см.eloquent-database.md - Индексы БД на WHERE/ORDER BY/JOIN — см.
../migrations.md - Кеширование дорогих запросов/вычислений
- Очереди для тяжёлых/медленных операций
- Production-оптимизации (opcache, config/route cache)
- Octane — для high-load (последний шаг, не первый)
Кеширование
php
// Redis cache tags — гранулярная инвалидация
Cache::tags(['organizations', "city:{$slug}"])->remember(
"orgs:{$slug}:page:{$page}",
now()->addHours(6),
fn () => Organization::forCity($slug)->paginate(20),
);
// Инвалидация только нужных тегов (НЕ Cache::flush() — убьёт сессии/throttle)
Cache::tags(['organizations'])->flush();- Ключи кеша — константы (
CacheKeys::ORG_LIST), не строки в разных местах. - Filament
afterSave()→ flush нужных тегов (см.../filament.md). - TTL осознанно: статика — часы/сутки, динамика — минуты.
- Tags работают только на Redis/Memcached (не file/database driver).
Очереди (Jobs)
Выносить в очередь: email/telegram/SMS, обработка файлов, внешние API, отчёты, импорт/экспорт.
php
final class ProcessImport implements ShouldQueue
{
public int $tries = 3;
public int $backoff = 30; // или [10, 30, 60] — прогрессивно
public function __construct(public int $importId) {} // ID, не модель-массив
public function handle(ImportService $service): void
{
$service->run($this->importId); // логика в Service, Job только диспатчит
}
public function failed(\Throwable $e): void { /* уведомить, пометить */ }
}$tries,$backoff,failed()— ВСЕГДА определять.- Передавать ID/модель (
SerializesModels), не массивы данных (stale + bloat). - Разделять очереди:
default,notifications,heavy— тяжёлое не блокирует быстрое. - Horizon для Redis-очередей (мониторинг, авто-балансировка воркеров, метрики).
Production-команды (деплой)
bash
php artisan config:cache # объединяет config в один файл (env() перестаёт работать!)
php artisan route:cache # кеш роутов (требует только closure-free routes)
php artisan view:cache # прекомпиляция Blade
php artisan event:cache # кеш event-listener маппинга
composer install --optimize-autoloader --no-dev
php artisan optimize # всё сразу (L11+)- После
config:cache—env()в коде возвращает null. Толькоconfig(). (ещё причина не использоватьenv()вне config). - opcache включён в prod (
opcache.enable=1, preloading для горячего кода).
Профилирование
| Инструмент | Назначение |
|---|---|
| Telescope | dev: запросы, N+1, jobs, исключения, request timeline |
| Pulse | production-friendly: slow queries, slow jobs, usage (легче Telescope) |
| Debugbar | dev: панель с запросами/таймингами в браузере |
| Laravel Boost MCP | database-query + EXPLAIN для CC-анализа (см. claude-code-workflow.md) |
DB::enableQueryLog() | точечный замер в коде |
- Профилировать ДО оптимизации (не угадывать узкое место).
- EXPLAIN на тяжёлых запросах — Seq Scan на больших таблицах = нужен индекс.
Octane (high-load, опционально)
- Держит приложение в памяти (Swoole/RoadRunner/FrankenPHP) — нет bootstrap на каждый request.
- Прирост x2-x4 RPS, НО: state leakage между запросами — статика, синглтоны с состоянием опасны.
- Применять когда уже выжаты N+1, кеш, индексы. Не «на старте».
- FrankenPHP — современный выбор (HTTP/3, встроен в образ).
Массовые операции
php
// Bulk insert/upsert — один запрос вместо N
Model::insert($rows); // без событий/timestamps
Model::upsert($rows, ['unique_col'], ['update_col']);
// Обход больших таблиц — chunkById/lazy, не get()
Model::lazy()->each(fn ($m) => ...); // генератор, не грузит всёAnti-patterns
| Паттерн | Проблема | Решение |
|---|---|---|
N+1 в Blade @foreach | сотни запросов | ->with() в контроллере |
Cache::flush() | убивает сессии/throttle/всё | Cache::tags([...])->flush() |
| Синхронный email в контроллере | HTTP висит 2-5с | Job/Queue |
get() на таблице >10k | OOM | chunkById() / lazy() |
Job без $tries/failed() | бесконечный retry / silent fail | всегда определять |
env() в коде после config:cache | возвращает null в prod | config() |
| Octane первым шагом | сложность без устранения N+1 | сначала базовая оптимизация |
| Cache file/database driver + tags | tags не работают | Redis |