A slow query is an obvious performance problem. A large response is a less obvious one, but it's still latency: every byte has to be transmitted, received, and parsed before your consumer can act on it.
GzipResponseMiddleware handles the transmission half of that automatically:
// api-infrastructure/src/Middleware/GzipResponseMiddleware.php
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
// ...
if (strlen($content) < $minSize || strlen($content) > $maxSize) {
return $response;
}
$compressed = gzencode($content, $level);
// ...
$response->setContent($compressed);
$response->headers->set('Content-Encoding', 'gzip');
return $response;
}
It's registered globally, not per-route:
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append([
SetRequestLocaleMiddleware::class,
GzipResponseMiddleware::class,
]);
})
$minSize: small responses skip compression entirely. Gzip has overhead of its own, and compressing a 200-byte error body saves nobody anything.
append(), not a route-scoped alias: this runs on every response in the app, the right default for a concern that has nothing to do with any one endpoint's business logic.
Compression fixes the bytes you're already sending. It doesn't fix sending bytes you didn't need to send. If a LicenseResource includes fields no consumer reads, that's payload you're compressing, transmitting, and parsing for nothing. The fix there isn't middleware, it's discipline in the Resource layer: return what the client uses, not everything the model happens to have.
Knowing The Optimization Worked
This is where the chapter closes the loop it opened. For anything that runs through the HTTP layer, you measured before you touched anything, because LogApiRequestsMiddleware was already recording execution_time and response_size on every request. Ship the fix, then check the same field.
Not everything runs through that middleware. ReconcileDomainsCommand is an artisan command, not an HTTP request, so it never touches execution_time. That doesn't mean it's unmeasured, it means you measure it the same way you measure anything: wrap the run in a timer and count the queries before you change anything. Same discipline, a different surface. Say the sweep from earlier was firing a thousand subscription queries against a thousand auto-renewing domains. Batch the check into chunks of 500, and the same sweep drops to two subscription queries per batch instead of one per domain. That's the number that tells you the fix worked, not a feeling that the code "looks better now."
If the number you're watching, execution_time on an endpoint or your own timer on a command, doesn't move, one of three things is true: you optimized the wrong query, the bottleneck was never the database (an external HTTP call, most likely), or the fix didn't actually ship. All three beat "I added batching, it should be faster now." Chapter 8 covers building dashboards on top of this data. The point here is simpler: measure whatever surface you're on, before and after, every time.
Performance Checklist
Before you touch anything:
- [X] Confirm the slow request in
execution_time, don't guess from a feeling - [X] Identify whether the cost is the database, an external call, or payload size
Query patterns:
- [X] Batch or eager load any query that would otherwise run once per row of a loop
- [X] Eager load a full relationship chain up front instead of letting each hop lazy-load
- [X] Cap the page size on any paginated endpoint a client controls
Caching:
- [X] Cache calls that are genuinely expensive (network I/O), not local lookups that are already fast
- [X] Every cache write has a matching
Cache::forget()at the site that invalidates it - [X] No cache key exists that you can't trace back to the write that clears it
Indexes:
- [X] Every index matches a real
WHEREclause, in the column order the query filters on - [X] No index added on a guess
After you ship:
- [X] Confirm
execution_timeactually moved - [X] If it didn't, find out why before you move on
Performance work that isn't measured isn't performance work. It's just code you changed and hoped about.