Keep Laravel away from static files. Serve from the edge.
Many Laravel apps serve CSS, JS, and images through Laravel itself (even if they're in public/, Laravel still boots on every request). A single page load might request 50 static assets. That's 50 unnecessary Laravel bootstraps per page view. With 10,000 visitors, that's 500,000 wasted Laravel requests daily.
public/, Laravel still bootsYour application server should NEVER serve static files. Nginx serves them locally. CDN serves them globally. Browser caches them permanently.
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com;
root /var/www/example.com/public;
index index.php;
# Static files - served directly by Nginx (no Laravel)
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
log_not_found off;
try_files $uri =404;
}
# HTML files (rare) - also serve directly
location ~* \.html$ {
expires 1h;
add_header Cache-Control "public";
}
# Dynamic requests - send to Laravel
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP processing
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
# 1. Change your DNS to CloudFlare
# 2. Enable "Proxy" (orange cloud) for your domain
# 3. Configure caching rules:
# CloudFlare Page Rules:
# URL: example.com/css/*
# Cache Level: Cache Everything
# Edge Cache TTL: 1 month
# URL: example.com/js/*
# Cache Level: Cache Everything
# Edge Cache TTL: 1 month
# URL: example.com/images/*
# Cache Level: Cache Everything
# Edge Cache TTL: 1 year
# 4. Configure Laravel to use CDN for assets
// config/app.php
'asset_url' => env('ASSET_URL', 'https://cdn.example.com'),
// .env
ASSET_URL=https://cdn.example.com
// In Blade (automatically prefixes CDN)
<link href="{{ asset('css/app.css') }}">
// Becomes: https://cdn.example.com/css/app.css
# For user-uploaded images (S3 + CloudFront)
# .env
AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=xxx
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=myapp-uploads
CLOUDFRONT_URL=https://d123.cloudfront.net
# config/filesystems.php
'disks' => [
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('CLOUDFRONT_URL'),
],
],
You set expires 1y — great! But now when you update app.css, users still see the old version for up to 1 year.
# webpack.mix.js (Laravel Mix)
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.version();
// Result:
// public/css/app.css?id=8a4b9c2d
// public/js/app.js?id=3e5f7a1b
// In Blade:
<link href="{{ mix('css/app.css') }}">
// Outputs: <link href="/css/app.css?id=8a4b9c2d">
// With CDN:
<link href="{{ asset(mix('css/app.css')) }}">
// Outputs: https://cdn.example.com/css/app.css?id=8a4b9c2d
# vite.config.js
export default defineConfig({
plugins: [laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
})],
});
// Vite automatically adds version hashes
// In Blade:
@vite(['resources/css/app.css', 'resources/js/app.js'])
// Outputs with version hashes automatically
| Asset Type | Cache-Control | Expires | Why |
|---|---|---|---|
| CSS, JS (versioned) | public, immutable |
1 year | Content never changes (new version = new filename) |
| Images, logos, icons | public |
1 year | Changes rarely |
| User-uploaded images | public |
1 month | Can change if user updates profile |
| HTML pages (dynamic) | no-cache |
0 | Always need fresh content |
| API responses | no-cache |
0 | Must be fresh |
# Complete Nginx cache configuration
location ~* \.(css|js)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ {
expires 1y;
add_header Cache-Control "public";
access_log off;
}
location ~* \.(woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public";
access_log off;
}
location / {
# Dynamic content - no cache
add_header Cache-Control "no-cache, private";
try_files $uri $uri/ /index.php?$query_string;
}
| Scenario | Without CDN | With CDN (CloudFlare/CloudFront) | Improvement |
|---|---|---|---|
| User in Tokyo loading CSS | 150ms (round-trip to US) | 5ms (Tokyo edge) | 30x faster |
| User in London loading JS | 80ms (to US) | 5ms (London edge) | 16x faster |
| User in Sydney loading image | 200ms (to US) | 5ms (Sydney edge) | 40x faster |
| Laravel CPU usage for static assets | 50ms per asset (50 assets = 2.5s CPU) | 0ms (Nginx/CDN) | 100% reduction |
| Bandwidth cost (100GB/month) | $9 (AWS data transfer) | $0 (CloudFlare free tier) | Free (or cheaper) |
| Overall Page Load Time | 2.5 seconds | 0.5 seconds | 80% faster |
CDN + Nginx static serving + browser caching can reduce page load time by 70-90% and reduce server load by 50-80%. It's the highest ROI performance optimization you can make.
| Layer | Responsibility | Cache Duration | Latency |
|---|---|---|---|
| Browser Cache | User's device | 1 year (versioned assets) | 0ms (local) |
| CDN Edge | 200+ global locations | 1 month - 1 year | 1-10ms |
| Nginx (Origin) | Application server | Static files only | 1ms |
| Laravel | NEVER for static assets | Dynamic content only | 50-200ms |
Topic 15: The 10 Disasters (Complete List) — The most common production failures. DB::enableQueryLog() in production. env() after config:cache. File sessions. Missing indexes. Full model serialization. And how to prevent each one.