Skip to content
prod e051e98
Browse

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.

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"]

Record the current Lighthouse scores before changing anything, so the gains are measurable.

  1. Record current Lighthouse scores. Temporarily set APP_DEBUG=false and clear the Laravel cache; run Lighthouse (npx lighthouse <url> or Chrome DevTools → Lighthouse); record mobile and desktop scores (Performance, Accessibility, Best Practices, SEO); re-enable APP_DEBUG=true locally; 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.

  1. Add Expires headers (browser caching).

    <IfModule mod_expires.c>
    ExpiresActive On
    ExpiresDefault "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/.htaccess after the security headers.
  2. 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.
  3. Add Gzip compression (fallback for direct origin access).

    <IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/css
    AddOutputFilterByType DEFLATE application/javascript text/javascript application/x-javascript
    AddOutputFilterByType DEFLATE application/json application/xml text/xml
    AddOutputFilterByType DEFLATE image/svg+xml
    SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|webp|pdf)$ no-gzip dont-vary
    </IfModule>
    • ✅ Text/JS/CSS responses are gzip-compressed at the origin.
  4. Disable ETags (in favor of Cache-Control).

    <IfModule mod_headers.c>
    Header unset ETag
    </IfModule>
    FileETag None
    • ✅ ETags are off; Cache-Control governs caching.
  5. Commit, deploy, and verify on production.

    Terminal window
    git add public/.htaccess
    git 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
    • curl confirms cache-control: public, max-age=2592000, immutable on assets.

Set required extensions and OpCache/memory options in the hosting panel, then verify OpCache on the server.

  1. 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.
  2. Set the runtime options & values.

    SettingValueNotes
    opcache.enableOnCritical for performance
    opcache.enable_cliOnHelps artisan commands
    display_errorsOffSecurity (production)
    expose_phpOffSecurity
    memory_limit512M256M minimum
    max_execution_time300For migrations/long tasks
    upload_max_filesize128MFor file uploads
    opcache.memory_consumption256OpCache memory
    opcache.max_accelerated_files20000Cached file count
    • ✅ The PHP options are set to the table values.
  3. Verify OpCache on the server.

    Terminal window
    ssh <SSH_ALIAS> "php -i | grep opcache.enable"
    # Expected: opcache.enable => On => On
    • opcache.enable reports On => On on the server.

Confirm Redis is the cache/session driver, then test a write/read.

  1. 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: redis
    php artisan tinker --execute="Cache::put('test', 'ok', 60); echo Cache::get('test');"
    # Expected: ok
    • ✅ Cache + session both report redis, and the write/read returns ok.

Turn on Brotli/Early Hints, respect existing cache headers, and add a landing-page page rule.

  1. Configure Cloudflare. Enable Brotli compression and Early Hints; set Browser Cache TTL to Respect Existing Headers (so the .htaccess rules 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: HIT confirms Cloudflare is caching the landing page.

Fix the high-leverage accessibility/performance/CSP items.

  1. Apply the quick-win fixes.

    FixChangeExpected gain
    Fix viewport metaRemove user-scalable=0, maximum-scale=1+5–8 Accessibility
    Add lazy loadingloading="lazy" on below-fold images+3–5 Performance
    Add CSP headerContent-Security-Policy in .htaccess+5–10 Best Practices
    Terminal window
    # Find viewport issues
    grep -rn "user-scalable\|maximum-scale" resources/views/ packages/
    # Find images without lazy loading
    grep -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.

Re-run Lighthouse against the baseline and confirm the cached targets.

  1. 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.

Do not mark this step done until every box below is checked.

  • 👤 Baseline scores recorded — mobile + desktop, before/after.
  • 🤖 .htaccess caching/compression livecurl confirms Cache-Control on assets.
  • 🔀 OpCache enabled — PHP limits and required extensions set.
  • 🤖 Redis confirmed — as cache + session driver (write/read passes).
  • 👤 Cloudflare Brotli + page rule livecf-cache-status: HIT.
  • 🤖 Quick wins applied — cached landing page sub-second.