πŸ“ Volume II: Laravel Performance Tuning Kit

πŸ—ΊοΈ Topic 2: Route Caching

The simplest 30ms you'll ever save.

"Every request, Laravel wakes up and reads every route file.
Stop making it think. Make it execute."
BEFORE WE START

Route caching is a single command that can reduce your request time by 20-40ms. It's the lowest-hanging fruit in Laravel performance tuning. Yet 90% of production Laravel apps don't have it enabled.

πŸ”΄ The Problem: Route Parsing on Every Request

WHAT LARAVEL DOES WITHOUT CACHE

On every single HTTP request, Laravel:

  1. Opens and reads routes/web.php (and routes/api.php)
  2. Parses every line looking for Route::get(), Route::post(), etc.
  3. Runs regex on each route pattern (e.g., /users/{id} β†’ #^/users/([^/]+)$#)
  4. Builds an internal routing table in memory
  5. Then discards it at the end of the request (because Laravel dies)

What a typical routes file looks like:

// routes/web.php β€” 200+ routes is normal for a medium app
Route::get('/', [HomeController::class, 'index']);
Route::get('/users', [UserController::class, 'index']);
Route::get('/users/{id}', [UserController::class, 'show']);
Route::post('/users', [UserController::class, 'store']);
Route::put('/users/{id}', [UserController::class, 'update']);
Route::delete('/users/{id}', [UserController::class, 'destroy']);
Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{slug}', [PostController::class, 'show']);
// ... 190 more routes with complex regex patterns
THE COST

For 200 routes: ~20-35ms of CPU time per request β€” just to figure out which controller to call. Multiply by 1 million requests per day = 20,000 seconds of wasted CPU daily.

🎨 What Happens Without Cache (Visualized)

β”‚ β”‚ WITHOUT ROUTE CACHE β€” Every request β”‚ ═══════════════════════════════════════════════════════════════════ β”‚ β”‚ HTTP Request: GET /users/123 β”‚ β”‚ β”‚ β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ Step 1: fopen('routes/web.php') β”‚ β”‚ β”‚ Step 2: Read entire file (I/O β€” expensive) β”‚ β”‚ β”‚ Step 3: Parse line 1: "Route::get('/'..." β”‚ β”‚ β”‚ Step 4: Parse line 2: "Route::get('/users'..." β”‚ β”‚ β”‚ Step 5: Parse line 3: "Route::get('/users/{id}'..." β”‚ β”‚ β”‚ Step 6: Parse line 4... (repeat 200 times) β”‚ β”‚ β”‚ Step 7: Run regex to extract {id} β†’ ([^/]+) β”‚ β”‚ β”‚ Step 8: Build routing array β”‚ β”‚ β”‚ Step 9: Match '/users/123' against 200 patterns β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β–Ό β”‚ Controller Found! (35ms wasted) β”‚ β”‚ ═══════════════════════════════════════════════════════════════════ β”‚ β”‚ WITH ROUTE CACHE β€” One request β”‚ ═══════════════════════════════════════════════════════════════════ β”‚ β”‚ HTTP Request: GET /users/123 β”‚ β”‚ β”‚ β–Ό β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ Step 1: require('bootstrap/cache/routes-v7.php') β”‚ β”‚ β”‚ Step 2: $routes = [ β”‚ β”‚ β”‚ 'GET' => [ β”‚ β”‚ β”‚ '/' => HomeController, β”‚ β”‚ β”‚ '/users' => UserController@index, β”‚ β”‚ β”‚ '/users/{id}' => UserController@show, β”‚ β”‚ β”‚ // ... precompiled array β”‚ β”‚ β”‚ ] β”‚ β”‚ β”‚ ] β”‚ β”‚ β”‚ Step 3: Match '/users/123' against array (2ms) β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β–Ό β”‚ Controller Found! (2ms) β”‚

βœ… The Fix: One Command

THE SOLUTION
php artisan route:cache

That's it. One command. Run it after you deploy your code.

What this command actually does:

// bootstrap/cache/routes-v7.php (generated file β€” DO NOT EDIT)
<?php return array (
  'GET' => 
  array (
    '/' => 
    array (
      'uses' => 'App\\Http\\Controllers\\HomeController@index',
      'middleware' => array (),
    ),
    '/users' => 
    array (
      'uses' => 'App\\Http\\Controllers\\UserController@index',
      'middleware' => array (),
    ),
    '/users/{id}' => 
    array (
      'uses' => 'App\\Http\\Controllers\\UserController@show',
      'where' => 
      array (
        'id' => '[0-9]+',
      ),
    ),
    // ... all 200 routes, precompiled as a PHP array
  ),
  'POST' => 
  array (
    '/users' => 
    array (
      'uses' => 'App\\Http\\Controllers\\UserController@store',
    ),
  ),
);
WHAT CHANGED

Instead of parsing regex and reading files on every request, Laravel now just require()s a precompiled PHP array. No regex. No file parsing. No I/O per request.

πŸ“Š The Numbers: Before vs After

πŸ‘Ž WITHOUT ROUTE CACHE

Routes parsed per request200
Regex operations200+
File I/O operations~10
Time per request (parsing only)25-35ms

πŸ‘ WITH ROUTE CACHE

Routes parsed per request1 (the cached array)
Regex operations0
File I/O operations1
Time per request (parsing only)1-3ms
MetricWithout CacheWith CacheImprovement
CPU time per request (route matching) 30ms 2ms 93% ↓
File I/O operations 10-20 1 90% ↓
Memory per request ~2MB (temporary) ~0.1MB (from cache) 95% ↓
10,000 requests per day 300 seconds (5 minutes of CPU) 20 seconds 280 seconds saved daily

✨ The Magic Command: One Command to Rule Them All

DON'T REMEMBER 5 COMMANDS
php artisan optimize

This single command runs:

⚠️ CRITICAL WARNING

Run php artisan optimize:clear before deployment, then php artisan optimize after deployment.

If you add a new route or change a config file, Laravel will still use the old cached version until you clear it. Always re-run optimize after any change in:

🎯 When to Use Route Caching (And When to Avoid)

βœ… ALWAYS USE in Production

  • Production servers
  • Staging environments
  • Any environment where routes don't change per request
  • API-heavy applications (every ms matters)

❌ DON'T USE in Development

  • Local development (you're adding routes constantly)
  • Testing environments where routes change
  • Reason: Laravel won't see new routes until you re-cache
PRO TIP

Add this to your deployment script (CI/CD):

#!/bin/bash
# deploy.sh
php artisan down --retry=60
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize          # ← This is critical
php artisan up

Never deploy without php artisan optimize. Never.

πŸ“ Topic 2 Summary: Route Caching

ConceptWithout CacheWith Cache
What Laravel does Parses 200+ routes with regex per request Loads precompiled PHP array
Time cost 25-35ms per request 1-3ms per request
When to run Never After every deployment, after any route change
Command β€” php artisan route:cache or php artisan optimize
πŸ“Œ THE RULE: Never run a Laravel production server without php artisan optimize.
It's free performance. 30ms saved per request. 10,000 requests/day = 5 minutes of CPU saved daily.
There is zero downside. Do it.
NEXT TOPIC PREVIEW

Topic 3: Config Caching β€” The env() trap that kills your config cache. Why config() is your best friend and env() is your enemy in production.