5 · Performance pass
Objective — capture a Lighthouse baseline, then layer .htaccess browser caching + compression, tune the PHP runtime (OpCache, memory, extensions), verify Redis, enable Cloudflare, and apply accessibility/CSP quick wins — so a vendor-bundled app gets measurable, verified performance gains.
Background
Section titled “Background”Record baselines first, then layer caching and tune the runtime. Benchmark with APP_DEBUG=false so debug overhead doesn’t pollute the numbers.
flowchart LR A["1. Baseline<br/>(Lighthouse)"] --> B["2. .htaccess<br/>caching + gzip"] B --> C["3. PHP runtime<br/>OpCache + limits"] C --> D["4. Redis"] D --> E["5. Cloudflare<br/>Brotli + page rule"] E --> F["6. Quick wins"] F --> G["7. Re-measure"]1. Capture a baseline
Section titled “1. Capture a baseline”Record the current Lighthouse scores before changing anything, so the gains are measurable.
-
Record current Lighthouse scores. Temporarily set
APP_DEBUG=falseand clear the Laravel cache; run Lighthouse (npx lighthouse <url>or Chrome DevTools → Lighthouse); record mobile and desktop scores (Performance, Accessibility, Best Practices, SEO); re-enableAPP_DEBUG=truelocally; also run against the production URL for comparison.- ✅ Mobile + desktop baseline scores are recorded for local and production.
2. Add browser caching & compression (.htaccess)
Section titled “2. Add browser caching & compression (.htaccess)”Add the caching/compression/ETag blocks to public/.htaccess after the security headers, then verify on production.
-
Add Expires headers (browser caching).
<IfModule mod_expires.c>ExpiresActive OnExpiresDefault "access plus 1 week"ExpiresByType text/html "access plus 0 seconds"ExpiresByType text/css "access plus 1 month"ExpiresByType application/javascript "access plus 1 month"ExpiresByType text/javascript "access plus 1 month"ExpiresByType image/jpeg "access plus 1 month"ExpiresByType image/png "access plus 1 month"ExpiresByType image/gif "access plus 1 month"ExpiresByType image/webp "access plus 1 month"ExpiresByType image/svg+xml "access plus 1 month"ExpiresByType image/x-icon "access plus 1 year"ExpiresByType font/woff2 "access plus 1 year"ExpiresByType font/woff "access plus 1 year"ExpiresByType font/ttf "access plus 1 year"ExpiresByType font/otf "access plus 1 year"ExpiresByType application/json "access plus 0 seconds"</IfModule>- ✅ The Expires block is in
public/.htaccessafter the security headers.
- ✅ The Expires block is in
-
Add Cache-Control headers (long TTL for static assets, no-cache for HTML).
<IfModule mod_headers.c><FilesMatch "\.(css|js|ico|pdf|jpg|jpeg|png|gif|webp|svg|woff|woff2|ttf|otf|eot)$">Header set Cache-Control "public, max-age=2592000, immutable"</FilesMatch><FilesMatch "\.(html|htm)$">Header set Cache-Control "no-cache, no-store, must-revalidate"Header set Pragma "no-cache"</FilesMatch></IfModule>- ✅ Static assets get a long immutable TTL; HTML is no-cache.
-
Add Gzip compression (fallback for direct origin access).
<IfModule mod_deflate.c>AddOutputFilterByType DEFLATE text/html text/plain text/cssAddOutputFilterByType DEFLATE application/javascript text/javascript application/x-javascriptAddOutputFilterByType DEFLATE application/json application/xml text/xmlAddOutputFilterByType DEFLATE image/svg+xmlSetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp|pdf)$ no-gzip dont-vary</IfModule>- ✅ Text/JS/CSS responses are gzip-compressed at the origin.
-
Disable ETags (in favor of Cache-Control).
<IfModule mod_headers.c>Header unset ETag</IfModule>FileETag None- ✅ ETags are off; Cache-Control governs caching.
-
Commit, deploy, and verify on production.
Terminal window git add public/.htaccessgit commit -m "perf: add .htaccess performance rules"curl -I https://<domain>/css/app.css | grep -i cache-control# Expected: cache-control: public, max-age=2592000, immutable- ✅
curlconfirmscache-control: public, max-age=2592000, immutableon assets.
- ✅
3. Tune the PHP runtime
Section titled “3. Tune the PHP runtime”Set required extensions and OpCache/memory options in the hosting panel, then verify OpCache on the server.
-
Enable the required extensions.
opcache,bcmath,ctype,curl,dom,fileinfo,gd,intl,json,mbstring,openssl,pdo,pdo_mysql,tokenizer,xml,zip.- ✅ All required extensions are enabled.
-
Set the runtime options & values.
Setting Value Notes opcache.enableOn Critical for performance opcache.enable_cliOn Helps artisan commands display_errorsOff Security (production) expose_phpOff Security memory_limit512M 256M minimum max_execution_time300 For migrations/long tasks upload_max_filesize128M For file uploads opcache.memory_consumption256 OpCache memory opcache.max_accelerated_files20000 Cached file count - ✅ The PHP options are set to the table values.
-
Verify OpCache on the server.
Terminal window ssh <SSH_ALIAS> "php -i | grep opcache.enable"# Expected: opcache.enable => On => On- ✅
opcache.enablereportsOn => Onon the server.
- ✅
4. Verify Redis
Section titled “4. Verify Redis”Confirm Redis is the cache/session driver, then test a write/read.
-
Confirm the drivers and test a write/read.
Terminal window php artisan tinker --execute="echo 'Cache: '.config('cache.default').', Session: '.config('session.driver');"# Expected: Cache: redis, Session: redisphp artisan tinker --execute="Cache::put('test', 'ok', 60); echo Cache::get('test');"# Expected: ok- ✅ Cache + session both report
redis, and the write/read returnsok.
- ✅ Cache + session both report
5. Enable Cloudflare
Section titled “5. Enable Cloudflare”Turn on Brotli/Early Hints, respect existing cache headers, and add a landing-page page rule.
-
Configure Cloudflare. Enable Brotli compression and Early Hints; set Browser Cache TTL to Respect Existing Headers (so the
.htaccessrules win); create a Page Rule for the landing page — Cache Everything, Edge TTL 2 hours.Terminal window curl -sI https://<domain> | grep cf-cache# Expected: cf-cache-status: HIT- ✅
cf-cache-status: HITconfirms Cloudflare is caching the landing page.
- ✅
6. Apply quick wins (~30 min)
Section titled “6. Apply quick wins (~30 min)”Fix the high-leverage accessibility/performance/CSP items.
-
Apply the quick-win fixes.
Fix Change Expected gain Fix viewport meta Remove user-scalable=0,maximum-scale=1+5–8 Accessibility Add lazy loading loading="lazy"on below-fold images+3–5 Performance Add CSP header Content-Security-Policyin.htaccess+5–10 Best Practices Terminal window # Find viewport issuesgrep -rn "user-scalable\|maximum-scale" resources/views/ packages/# Find images without lazy loadinggrep -rn "<img" resources/views/ packages/ | grep -v "loading="# Expected: the grep surfaces the viewport + non-lazy-loaded image sites to fix- ✅ Viewport meta is fixed, below-fold images lazy-load, and a CSP header is set.
7. Re-measure
Section titled “7. Re-measure”Re-run Lighthouse against the baseline and confirm the cached targets.
-
Re-measure and compare. Re-run Lighthouse on production (mobile + desktop) and compare against the baseline; cross-check with PageSpeed Insights.
- ✅ Landing page < 0.5s cached, login < 1s.
Checklist
Section titled “Checklist”Do not mark this step done until every box below is checked.
- 👤 Baseline scores recorded — mobile + desktop, before/after.
- 🤖
.htaccesscaching/compression live —curlconfirmsCache-Controlon assets. - 🔀 OpCache enabled — PHP limits and required extensions set.
- 🤖 Redis confirmed — as cache + session driver (write/read passes).
- 👤 Cloudflare Brotli + page rule live —
cf-cache-status: HIT. - 🤖 Quick wins applied — cached landing page sub-second.