Lesson 33 — Laravel performance: Start with a specific request, controlled data and a repeatable metric. TaskFlow's project page should keep query counts stable as task count increases; neither latency nor memory usage necessarily stays constant.
1. Ask a measurable question
ProjectQueryBudgetTest creates a project whose tasks have assignees and one attachment each. It logs queries only during an HTTP GET with per_page=20, first with one task and then with twenty. Fixture creation and migrations are outside the measured region. Array sessions and actingAs exclude costs present in a real authenticated request.
Both requests execute exactly five queries: route-bound project, pagination count, tasks, assignees and attachments. The test checks equality and a six-query review budget. The exact-five assertion records this checkpoint; legitimate feature changes require reviewing the measurement rather than blindly raising thresholds.
2. Why no per-task query growth?
// ProjectController::show: authorize first, paginate, then eager-load.
Gate::authorize('view', $project);
$tasks = $query->paginate($project, $request->query());
$tasks->getCollection()->load('attachments');
foreach ($tasks as $task) {
$task->setRelation('project', $project);
}
return view('projects.show', ['project' => $project, 'tasks' => $tasks]);
// TaskListQuery keeps assignee keys needed by the relationship:
$query = $project->tasks()->with('assignee:id,name');TaskListQuery eager-loads assignees and the controller batch-loads attachments. Policies inside Blade access each task's project; setRelation reuses the already authorized parent. This is valid because the task query was scoped to that exact project. Never attach an arbitrary parent to bypass authorization.
Lesson 12 measured a five-task example dropping from six to two queries. This lesson measures the actual HTTP page with Blade, policies and attachments instead of extrapolating a small query demonstration. Inspect policies and components as well as controllers when tracking N+1 behavior.
3. Fewer queries do not guarantee less work
Tasks are capped at 50 per page, but attachment count per task currently has no quota or separate pagination. One eager-load query can return many rows. The one-attachment fixture does not prove scalability to thousands of files. Consider withCount on summary pages and a paginated attachment endpoint when requirements justify it.
Count queries, deep offsets, leading-wildcard LIKE searches and title sorting can remain expensive at a fixed query count. Inspect EXPLAIN on the target engine with realistic data before adding indexes. Cursor pagination changes navigation contracts and needs stable ordering. Indexing every column increases write and storage costs.
4. Measure latency and memory separately
On staging, record p50/p95 latency, error rate, returned rows, query time, peak memory, queue lag and response bytes. Compare equivalent datasets, runtime versions, warm/cold cache states and concurrency. Test-suite duration is not HTTP latency. Do not load-test production without an approved load plan.
DB::enableQueryLog retains queries and bindings in memory. The regression uses try/finally to disable and flush it. Unbounded production logging can consume memory and leak personal data or credentials; use sampling, retention limits and sensitive-data filtering.
5. Cache the right layer
ProjectTaskCount uses a project-specific key with a 30-second TTL; the maintenance job invalidates it after task creation. This count may be stale and must not determine authorization or transactional correctness. In-flight readers can repopulate stale values, and crashes before enqueue can lose invalidation. Never share an owner-specific page cache without the necessary user scope.
Application data caches differ from framework configuration, route, view and event caches. In a correctly configured staging release, inspect these steps independently:
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
After configuration caching, business code should read config rather than env. Rebuild caches and restart long-lived processes as part of a deliberate deployment. optimize:clear also clears keys from the default cache driver; it is not a harmless file-only operation. This lesson runs no production cache commands and changes no FPM/OPcache settings.
6. Preserve improvements with regression tests
php artisan test --filter=ProjectQueryBudgetTest
php artisan test --filter=TaskFlowLoadingTest
php artisan test
vendor/bin/pint --test
The checkpoint passes 98 tests/442 assertions and Pint. Existing eager loading needed no application-code change; the addition is an actual-page query regression. Concurrent load, server p95, MySQL/PostgreSQL behavior, large attachment cardinality and OPcache benefits remain unmeasured.
Exercise: increase attachment cardinality and compare query count with hydrated model count. On an experimental branch, remove eager loading, observe the regression failure and restore it. State a metric's limits before claiming a speedup. Next comes deployment and worker/scheduler operation.
References: Eloquent relationships, Laravel deployment. Navigation: Lesson 32: CI · Roadmap.




No comments yet. Be the first to share your thoughts.