Laravel Vitals
Toggle theme

Learn

Reference for every issue Laravel Vitals can detect

73 known issues
All Performance Accessibility Best Practices SEO

performance

38 items

Reduce unused JavaScript

Warning
156 active in your app unused-javascript

JavaScript shipped to the browser but never executed wastes bandwidth and parse time.

Unused JavaScript still costs network bandwidth, parse time, and CPU. The browser must download, parse, and compile every byte you ship — even if it never executes.

Typical savings: 30-60% bundle size, 200-500ms LCP improvement
Recommended
// Lazy-load route-specific JS
const Dashboard = () => import('./Dashboard.js');

// Or use @vite directive in Blade
@vite(['resources/js/app.js'])
Avoid
<!-- Loading the entire bundle on every page -->
<script src="/build/everything.js"></script>

Remove unused CSS

Warning
unused-css-rules

Unused CSS bytes still need to be downloaded and parsed by the browser.

Unused CSS rules force the browser to parse selectors that never match, delay first paint, and bloat the critical render path.

Recommended
// tailwind.config.js — content paths trigger purging
content: ['./resources/**/*.blade.php']
Avoid
/* Loading the full Bootstrap CSS bundle */
@import 'bootstrap/dist/css/bootstrap.min.css';

Minify JavaScript

Warning
unminified-javascript

Minified JavaScript reduces transfer size with no behaviour change.

Minified JavaScript reduces transfer size by ~30-50% (whitespace, comments, mangled names) without changing behaviour.

Recommended
// vite.config.js (default in production)
build: { minify: 'esbuild' }
Avoid
// Disabling minification in production
build: { minify: false }

Minify CSS

Warning
unminified-css

Minified CSS reduces transfer size.

Minified CSS shrinks transfer size and parsing time. Vite and Laravel Mix do this by default in production builds.

Eliminate render-blocking resources

Warning
16 active in your app render-blocking-resources

Resources in the head block first paint until they download. Defer or inline critical CSS.

Render-blocking resources in <head> delay first paint until the browser downloads and parses them. Every kilobyte adds latency, especially on slow networks.

Recommended
<!-- Defer non-critical scripts -->
<script src="analytics.js" defer></script>

<!-- Async for independent scripts -->
<script src="ads.js" async></script>
Avoid
<!-- Blocks parsing until downloaded -->
<script src="jquery.js"></script>

Serve images in next-gen formats

Info
modern-image-formats

WebP and AVIF compress better than JPEG/PNG.

WebP and AVIF compress 25-50% smaller than JPEG/PNG at equivalent quality. Lower bytes = faster LCP on image-heavy pages.

Recommended
<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" alt="Hero">
</picture>
Avoid
<img src="hero.jpg" alt="Hero"><!-- 800 KB JPEG -->

Use responsive images

Info
uses-responsive-images

Add srcset and sizes so the browser picks the best variant for each viewport.

Serving a 2400px image to a 360px mobile screen wastes 4-8x the bandwidth. srcset tells the browser which variant to fetch based on viewport.

Recommended
<img
  src="hero-800w.jpg"
  srcset="hero-400w.jpg 400w, hero-800w.jpg 800w, hero-1600w.jpg 1600w"
  sizes="(max-width: 600px) 400px, 800px"
  alt="Hero">
Avoid
<img src="hero-2400w.jpg" alt="Hero">

Use video for animated content

Info
efficient-animated-content

Animated GIFs are heavy. Encode the animation as MP4/WebM and use <video>.

Animated GIFs are 5-10x larger than equivalent MP4 video. Convert to WebM/MP4 and use a <video> element.

Recommended
<video autoplay loop muted playsinline>
  <source src="animation.webm" type="video/webm">
  <source src="animation.mp4" type="video/mp4">
</video>
Avoid
<img src="animation.gif" alt="Animation"><!-- 8 MB -->

Lazy-load offscreen images

Info
offscreen-images

Add loading="lazy" so below-the-fold images are fetched on demand.

Lazy-loading defers off-screen images until the user scrolls near them. Reduces initial payload and frees up the network for above-the-fold content.

Recommended
<img src="below-fold.jpg" loading="lazy" alt="...">
Avoid
<img src="below-fold.jpg" alt="..."><!-- loaded eagerly -->

Avoid serving legacy JavaScript to modern browsers

Warning
legacy-javascript

Polyfilled bundles bloat modern browser payloads.

Polyfilled bundles (for IE11, etc.) bloat modern browser payloads. Use module/nomodule pattern to ship clean ES2017+ to evergreen browsers.

Recommended
// vite.config.js
build: { target: 'es2017' }
Avoid
// Forcing ES5 transpilation for everyone
build: { target: 'es5' }

Remove duplicated modules

Info
duplicated-javascript

Multiple bundles include the same module. Tune Vite vendor splitting.

When multiple bundles include the same module, users download the code twice. Vite vendor splitting deduplicates shared dependencies.

Enable text compression

Warning
19 active in your app uses-text-compression

Compress text-based responses with gzip or Brotli to reduce transfer size significantly.

Text-based resources (HTML, CSS, JS, JSON) compress 60-80% with gzip and even better with Brotli. Without compression every byte travels over the wire as-is, slowing FCP and LCP directly.

Typical savings: 60-80% transfer size on JS/CSS, 150-400ms LCP improvement
Recommended
# nginx — enable gzip + Brotli
gzip on;
gzip_types text/plain text/css application/javascript application/json;
gzip_min_length 1024;

# Brotli (ngx_brotli module)
brotli on;
brotli_types text/plain text/css application/javascript;
Avoid
# No gzip / Brotli block in nginx.conf
# Responses delivered uncompressed — 3-5x larger than necessary

Efficiently encode images

Warning
17 active in your app uses-optimized-images

Unoptimized images contain redundant data. Compress them to improve LCP.

Unoptimized JPEG/PNG images contain metadata and redundant pixel data. Lossless or lossy compression can cut size 30-60% with no perceptible quality difference — directly improving LCP.

Typical savings: 30-70% image size, significant LCP improvement on image-heavy pages
Recommended
// Intervention Image v3 — optimize on upload
use Intervention\Image\ImageManager;
use Intervention\Image\Drivers\Gd\Driver;

$manager = new ImageManager(new Driver());
$manager->read($path)
    ->toWebp(quality: 80)
    ->save(storage_path('app/public/images/hero.webp'));
Avoid
// Storing raw upload with no compression
$request->file('image')->store('images'); // may be a 4 MB JPEG

Preconnect to required origins

Info
uses-rel-preconnect

Add <link rel="preconnect"> for third-party origins so DNS/TCP/TLS handshakes happen early.

Each connection to a third-party origin (fonts.googleapis.com, cdn.jsdelivr.net) requires a DNS lookup + TCP handshake + TLS negotiation — often 100-300ms. `<link rel="preconnect">` initiates these during HTML parsing, before the resource is actually requested.

Typical savings: 100-300ms per third-party origin on initial load
Recommended
{{-- In Blade <head> --}}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="dns-prefetch" href="https://cdn.jsdelivr.net">
Avoid
{{-- No preconnect: browser discovers the origin only when parsing the font link --}}
<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet">

Prioritize the LCP image

Warning
prioritize-lcp-image

Add fetchpriority="high" to the hero image so the browser fetches it before lower-priority resources.

The LCP element is the most critical resource for user-perceived load speed. Adding `fetchpriority="high"` tells the browser to fetch it immediately, above other resources with the same priority, shaving 100-500ms off LCP.

Typical savings: 100-500ms LCP
Recommended
{{-- Hero image — the likely LCP element --}}
<img
  src="/images/hero.webp"
  fetchpriority="high"
  loading="eager"
  decoding="async"
  width="1200" height="600"
  alt="Hero">
Avoid
{{-- No priority hint — browser treats it equally with other resources --}}
<img src="/images/hero.webp" alt="Hero">

Minimize main-thread work

Warning
mainthread-work-breakdown

Heavy JavaScript execution blocks rendering and interaction. Code-split and defer non-critical scripts.

Heavy JavaScript parsing, compiling, and executing on the main thread blocks all rendering and user interaction. Tasks over 50ms are "long tasks" that cause jank. Code-splitting and deferring non-critical JS are the primary fixes.

Typical improvement: reduces TBT by 200-800ms, INP drops to < 200ms
Recommended
// Break up heavy work with scheduler.yield() (Chrome 115+)
async function processData(items) {
    for (const item of items) {
        process(item);
        await scheduler.yield(); // yield between items
    }
}

// Vite manual chunks to split vendor code
rollupOptions: { output: { manualChunks: { vendor: ['lodash', 'moment'] } } }
Avoid
// Heavy synchronous loop on page load blocks the main thread
const result = massiveDataSet.map(computeExpensive);

Avoid excessive DOM size

Warning
dom-size

A large DOM slows style calculation and layout. Paginate long lists or use virtual scrolling.

Pages with > 1500 DOM nodes suffer slower style recalculation, layout, and paint. Every queried selector must traverse the whole tree. Paginate large lists or use virtual scrolling.

Typical improvement: style recalculation 2-5x faster, smoother scrolling
Recommended
// Paginate results in Laravel
$items = Item::query()->paginate(50);

// Livewire lazy loading for heavy components
#[Lazy]
class HeavyList extends Component {}
Avoid
@foreach ($allItems as $item)
    {{-- Rendering 5000+ items at once bloats the DOM --}}
    <div class="item">{{ $item->name }}</div>
@endforeach

Avoid multiple page redirects

Warning
redirects

Each redirect adds round-trip latency. Eliminate chains and enforce HTTPS at the server level.

Each HTTP redirect adds a full round-trip — typically 100-300ms on mobile. Redirect chains multiply this cost. The most common Laravel culprit is HTTP→HTTPS redirect that could be eliminated with HSTS or server-level HTTPS enforcement.

Typical savings: 100-300ms per eliminated redirect
Recommended
# Enforce HTTPS at the server level (not in PHP)
# nginx:
server {
    listen 80;
    return 301 https://$host$request_uri;
}

# With HSTS, browsers skip the redirect entirely on repeat visits:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
Avoid
// Multiple redirect hops:
// http://example.com -> https://example.com -> https://www.example.com -> /home
// Each hop adds a full RTT

Reduce server response time (TTFB)

Warning
server-response-time

High TTFB drags down LCP. Enable OPcache, config/route cache, and use Redis for sessions.

A TTFB > 600ms directly drags down LCP — the browser cannot start rendering until the first byte arrives. Laravel-specific causes include disabled OPcache, no config/route cache, slow database queries, and no Redis for sessions/cache.

Typical savings: 100-500ms TTFB with OPcache + config cache
Recommended
# Full deploy-time optimization
php artisan optimize   # config + route + view cache

; php.ini (critical for any PHP app)
opcache.enable=1
opcache.validate_timestamps=0
opcache.memory_consumption=256

# Redis for sessions + cache
SESSION_DRIVER=redis
CACHE_STORE=redis
Avoid
# No caches, default file session/cache, APP_DEBUG=true
# Every request re-reads config files and re-registers routes

Use passive event listeners

Info
uses-passive-event-listeners

Touch and wheel listeners without { passive: true } block scrolling. Add the passive flag.

Touch and wheel event listeners without `{ passive: true }` block the browser from scrolling until the handler completes. This forces the browser to wait up to 50ms per scroll event, causing visible jank.

Typical improvement: scroll jank eliminated, INP improves by 30-100ms
Recommended
// Passive listener — browser can scroll immediately
document.addEventListener('touchstart', handler, { passive: true });
document.addEventListener('wheel', handler, { passive: true });
Avoid
// Non-passive — browser waits for handler before scrolling
document.addEventListener('touchstart', handler); // defaults to passive: false

Avoid the deprecated write() API

Warning
no-document-write

The deprecated parser-blocking API should be replaced with DOM manipulation methods.

The deprecated `document.write()` API blocks HTML parsing because the browser cannot continue until the injected content is processed. On slow connections this can pause rendering by seconds. Use DOM manipulation instead.

Removes potentially seconds of blocking time on 2G/3G connections
Recommended
// Use DOM manipulation instead
const el = document.createElement('script');
el.src = 'analytics.js';
el.async = true;
document.head.appendChild(el);
Avoid
// Deprecated API that blocks HTML parsing
// document.write('<script src="analytics.js"></script>');

Serve static assets with an efficient cache policy

Info
uses-long-cache-ttl

Short cache TTLs force re-downloads on repeat visits. Use long TTL with hashed filenames.

Short cache TTLs on static assets force re-downloads on repeat visits, wasting bandwidth and slowing pages for returning users. Vite's content-hashed filenames make it safe to cache assets for a year.

Repeat visits load 90-100% from cache — near-instant for returning users
Recommended
# nginx — 1-year cache for Vite-hashed assets
location ~* /build/assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}
Avoid
# No explicit cache headers — browsers re-validate every request
location /build/ {
    # missing expires / Cache-Control
}

LCP image should not be lazy-loaded

Warning
lcp-lazy-loaded

Remove loading="lazy" from your LCP image — it delays the most important resource on the page.

The LCP image has `loading="lazy"` which intentionally delays its fetch. The browser won't load it until it becomes visible — but it's the most important resource on the page. Remove `loading="lazy"` from above-the-fold images.

Typical savings: 200-800ms LCP
Recommended
{{-- Hero / LCP image: eager + high priority --}}
<img src="hero.webp" fetchpriority="high" loading="eager" alt="Hero">
Avoid
{{-- Do NOT lazy-load your LCP image --}}
<img src="hero.webp" loading="lazy" alt="Hero"><!-- delays LCP! -->

Largest Contentful Paint element identified

Info
largest-contentful-paint-element

Optimise the LCP element: preload, fetchpriority="high", WebP format, and no lazy-load.

Knowing which element is the LCP lets you focus optimization efforts. Common fixes: preload the image, remove lazy-load, upgrade to WebP, preconnect to its origin, or inline its critical CSS.

Recommended
{{-- Once you know the LCP element, apply these optimizations --}}
<img
  src="/images/hero.webp"
  fetchpriority="high"
  loading="eager"
  width="1200" height="600"
  alt="Hero">

Avoid large layout shifts

Warning
layout-shift-elements

Elements causing CLS are identified. Reserve space with explicit dimensions or aspect-ratio CSS.

CLS is caused by elements shifting after initial render — typically images without dimensions, dynamically injected banners, or web fonts causing text reflow. Identifying the shifting element is the first step to fixing CLS.

Fixing the top shift element typically drops CLS from 0.2-0.4 to under 0.1
Recommended
{{-- Reserve space before the image loads --}}
<img src="product.jpg" width="400" height="300" alt="Product">

{{-- Or with CSS aspect-ratio --}}
<div style="aspect-ratio: 4/3; overflow: hidden">
  <img src="product.jpg" alt="Product">
</div>
Avoid
{{-- No dimensions — causes layout shift when image loads --}}
<img src="product.jpg" alt="Product">

Avoid non-composited animations

Info
non-composited-animations

Animate only transform and opacity to keep animations on the GPU compositor thread.

Animations that trigger layout or paint (animating `top`, `left`, `width`, `height`) run on the main thread and cause jank. Animating only `transform` and `opacity` allows the browser to offload work to the GPU compositor thread.

Typical improvement: animation FPS from 30-40 to 60fps
Recommended
/* GPU-composited — runs off main thread */
@keyframes slideIn {
    from { transform: translateX(-100%); }
    to   { transform: translateX(0); }
}
Avoid
/* Triggers layout — runs on main thread, causes jank */
@keyframes slideIn {
    from { left: -100%; }
    to   { left: 0; }
}

Serve images at the right size

Info
image-size-responsive

Use srcset and sizes to serve appropriately-sized images for each viewport.

Serving a 2000px image to a 400px mobile device wastes 4-5× bandwidth. Use `srcset` and `sizes` to let the browser choose the best-fit image for the current viewport.

Typical savings: 60-80% bytes on mobile viewports
Recommended
<img
  src="/images/hero-800w.jpg"
  srcset="/images/hero-400w.jpg 400w, /images/hero-800w.jpg 800w, /images/hero-1600w.jpg 1600w"
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1600px"
  alt="Hero image">
Avoid
<!-- Always serving the full-resolution image -->
<img src="/images/hero-4000w.jpg" alt="Hero">

N+1 query pattern detected

Warning
60 active in your app n-plus-one-detected

A query pattern repeated above the configured threshold during this audit.

N+1 queries happen when accessing a relation inside a loop without eager loading. Each iteration triggers a separate SQL query — 100 items = 101 queries.

Typical savings: cuts query count by 90%, 200-1500ms TTFB improvement
Recommended
// Eager-load the relationship
$users = User::with('posts')->get();
foreach ($users as $user) {
    echo $user->posts->count();
}
Avoid
// 1 + N queries — fires a SELECT for each user
$users = User::all();
foreach ($users as $user) {
    echo $user->posts->count();
}

Slow database queries detected

Warning
slow-queries-detected

One or more queries exceeded the slow-query threshold during this audit.

Queries over 50ms typically indicate missing indexes, full table scans, or inefficient joins. Use EXPLAIN to diagnose, then add indexes or rewrite.

Recommended
// In a migration — add an index
Schema::table('orders', fn ($t) => $t->index(['user_id', 'created_at']));

Slow Blade views detected

Info
slow-views

A rendered view took longer than the configured threshold.

Views that take > 50ms to render usually loop over collections without `lazy()`, render expensive partials, or call relationships not eager-loaded.

Real-world performance worse than synthetic

Warning
real-world-perf-degraded

Real-traffic telemetry (Pulse / Telescope) shows P95 metrics significantly above the synthetic Lighthouse audit. Investigate production-only conditions: load, third-party scripts, geography.

Synthetic Lighthouse audits run on a clean machine with stable network. Real production traffic faces variable load, geographic latency, and contention — surfacing problems synthetic tests miss.

Third-party scripts blocking the main thread

Warning
third-party-blocking

:count third-party origin(s) (:entities) block the main thread > 250ms. Defer or self-host where possible.

Third-party scripts (analytics, ads, chat widgets) often run synchronously on page load and steal main-thread time, delaying interactivity. Defer or self-host critical ones.

Recommended
<!-- Defer non-critical 3rd-party JS -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>

Reduce page weight

Warning
large-payload

Total page weight is :mb MB. Large payloads hurt LCP on slow connections. Compress images and split JavaScript bundles.

Large pages (> 2 MB) hurt LCP on 3G/4G connections. Typical culprits: uncompressed images, oversized JS bundles, unused vendor libraries shipped to all pages.

Reduce JavaScript execution time

Warning
bootup-time-high

A single script takes :ms ms to evaluate. Code-split, lazy-load, or remove unused JavaScript.

A single script taking >500ms to evaluate blocks the main thread for that duration. Code-splitting separates critical and deferred logic so the page becomes interactive sooner.

Recommended
// vite.config.js — split vendor chunks
rollupOptions: {
    output: {
        manualChunks: { vendor: ['react', 'react-dom'] }
    }
}

Image elements have explicit width and height

Warning
unsized-images

Set explicit width/height attributes on images to reserve layout space. Browsers can compute the aspect ratio early and avoid layout shifts when the image loads.

When the browser parses an <img> without explicit dimensions, it has to wait until the image downloads before knowing how much space to reserve. The result: visible content jumps when the image arrives — high CLS.

Typical savings: cuts CLS from 0.2-0.4 to under 0.1
Recommended
<!-- Browser reserves space immediately -->
<img src="hero.jpg" width="1200" height="600" alt="...">

<!-- Or with CSS aspect-ratio -->
<img src="hero.jpg" style="aspect-ratio: 2/1; width: 100%;" alt="...">
Avoid
<img src="hero.jpg" alt="..."><!-- causes layout shift -->

Ensure text remains visible during webfont load

Info
font-display

Use `font-display: swap` so the browser shows a fallback font immediately and swaps to the webfont when ready. Avoids invisible text (FOIT).

Without `font-display: swap`, browsers may show invisible text until the webfont loads (FOIT — Flash of Invisible Text). With `swap`, fallback text is visible immediately and the webfont swaps in when ready.

Recommended
@font-face {
    font-family: 'Inter';
    src: url('/fonts/inter.woff2') format('woff2');
    font-display: swap;
}
Avoid
@font-face {
    font-family: 'Inter';
    src: url('/fonts/inter.woff2') format('woff2');
    /* no font-display => browser default 'block' = FOIT */
}

Preload key requests

Info
uses-rel-preload

Add `<link rel="preload">` for resources discovered late in the page (e.g. Vite-emitted modules, hero images, critical fonts). The browser fetches them earlier.

The browser discovers resources by parsing HTML and CSS in order. Late-discovered resources (e.g. fonts referenced inside @font-face) start downloading too late. `<link rel="preload">` tells the browser to fetch them in parallel with the initial HTML.

Typical savings: 100-400ms LCP on font-heavy pages
Recommended
{{-- In Blade layout --}}
<head>
    @vite(['resources/js/app.js'])
    <link rel="preload" as="font" type="font/woff2" href="/fonts/inter.woff2" crossorigin>
    <link rel="preload" as="image" href="/images/hero.webp">
</head>

Consider Laravel Octane for lower TTFB

Info
octane-not-running

Octane keeps the application bootstrapped between requests (Swoole / FrankenPHP / RoadRunner), eliminating the per-request bootstrap cost. Typical TTFB savings: 40-200ms.

Each Laravel request bootstraps the framework: loading config, registering service providers, and parsing routes. Octane keeps the application in memory across requests via Swoole / FrankenPHP / RoadRunner, slashing bootstrap time. Most useful for high-traffic apps where TTFB matters.

Typical savings: 40-200ms per request TTFB
Recommended
# .env
OCTANE_SERVER=frankenphp

# Install + run
composer require laravel/octane
php artisan octane:install
php artisan octane:start

accessibility

10 items

Add alt text to images

Warning
image-alt

Images without alt text are inaccessible to screen readers.

Screen readers cannot describe images without alt text. Decorative images should use alt="" to be skipped.

Recommended
<img src="product.jpg" alt="Red Nike Air Max sneakers">
<img src="divider.svg" alt=""><!-- decorative -->
Avoid
<img src="product.jpg"><!-- no alt -->

Add a document title

Critical
document-title

Pages without a <title> are inaccessible and bad for SEO.

Pages without <title> are inaccessible (screen readers announce "Untitled") and crippling for SEO. Search engines use it as the result snippet.

Recommended
<title>Product page — Acme Store</title>

Declare a document language

Critical
html-has-lang

Add <html lang="..."> for assistive technologies.

Without <html lang="..."> screen readers fall back to the user's default language pronunciation, which can mangle text in other languages.

Recommended
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
Avoid
<html><!-- no lang -->

Buttons have an accessible name

Warning
button-name

Buttons without a label are announced as "button" with no context. Add text or aria-label.

Buttons without accessible names are announced as "button" by screen readers. Users with visual impairments cannot tell what action will occur. Always provide text content or `aria-label`.

Recommended
<!-- With visible label -->
<button type="button">Save changes</button>

<!-- Icon-only button with aria-label -->
<button type="button" aria-label="Delete item">
    <svg aria-hidden="true">...</svg>
</button>
Avoid
<!-- Screen reader says 'button' with no context -->
<button type="button"><svg>...</svg></button>

Has a viewport meta tag

Warning
meta-viewport

Add <meta name="viewport" content="width=device-width, initial-scale=1"> for proper mobile rendering.

Without a correct viewport meta tag, mobile browsers render the page at desktop width and scale it down. Users see tiny text and must pinch-to-zoom. `user-scalable=no` also fails accessibility requirements.

Recommended
<meta name="viewport" content="width=device-width, initial-scale=1">
Avoid
<!-- Missing viewport tag, or disabling zoom -->
<meta name="viewport" content="width=device-width, user-scalable=no"><!-- fails a11y -->

html element has a valid lang attribute

Warning
html-lang-valid

The lang attribute value must be a valid BCP47 language code (e.g. en, fr, pt-BR).

An invalid BCP47 language code (e.g. `lang="en-123"` or `lang="fr-INVALID"`) means assistive technologies fall back to default pronunciation. Use the standard code for your language.

Recommended
<!-- Valid BCP47 codes: en, fr, de, es, zh-Hans, pt-BR -->
<html lang="en">
<html lang="fr">
<html lang="pt-BR">
Avoid
<!-- Incorrect / made-up subtags -->
<html lang="EN-US-INVALID">

ARIA roles have required attributes

Warning
aria-required-attr

Elements with ARIA roles must include all required attributes for assistive technologies.

ARIA roles require certain attributes to function correctly. For example, `role="checkbox"` requires `aria-checked`. Missing required attributes break assistive technology semantics entirely.

Recommended
<!-- role=checkbox requires aria-checked -->
<div role="checkbox" aria-checked="false" tabindex="0">Option A</div>

<!-- role=combobox requires aria-expanded -->
<input role="combobox" aria-expanded="false" aria-autocomplete="list">
Avoid
<!-- Missing required attribute -->
<div role="checkbox">Option A</div><!-- no aria-checked -->

ARIA attributes have valid values

Warning
aria-valid-attr-value

ARIA attribute values must come from the allowed set defined by the ARIA specification.

ARIA attribute values must be from the allowed set. An invalid value (e.g. `aria-live="sometimes"`) is ignored by screen readers, silently breaking the intended accessible behaviour.

Recommended
<!-- Valid values only -->
<div aria-live="polite">Loading…</div>
<button aria-expanded="true">Menu</button>
Avoid
<!-- Invalid values — ignored by assistive tech -->
<div aria-live="sometimes">Loading…</div>
<button aria-expanded="yes">Menu</button>

best practices

20 items

Fix browser console errors

Info
errors-in-console

Console errors hint at runtime bugs that can degrade UX.

Console errors signal runtime bugs that degrade UX silently. Treat them as bugs to fix before they reach production.

Update vulnerable JavaScript libraries

Critical
no-vulnerable-libraries

A bundled library has a known vulnerability. Upgrade or remove it.

A bundled JS library with a known CVE puts every visitor at risk. Run `npm audit` regularly and ship updates promptly.

Use HTTPS

Critical
is-on-https

Serve all resources over HTTPS to protect users and enable modern browser features.

HTTP pages cannot use many modern browser features (Service Workers, HTTP/2 push, geolocation). Browsers mark them as "Not Secure" — destroying user trust. All production Laravel apps should be served over HTTPS.

Recommended
# nginx with Let's Encrypt
server {
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}

# Force HTTPS in Laravel
// AppServiceProvider::boot()
\URL::forceScheme('https');
Avoid
# Serving over plain HTTP — browser shows 'Not Secure'
server {
    listen 80;
    # no SSL block
}

Avoid requesting geolocation on page load

Warning
geolocation-on-start

Request geolocation only after a clear user interaction, not immediately on page load.

Requesting geolocation permission immediately on page load (before any user interaction) is considered intrusive. Browsers show a permission prompt that most users dismiss — then the feature is blocked forever. Request only after a clear user action.

Recommended
// Request on explicit user action
document.getElementById('find-me').addEventListener('click', () => {
    navigator.geolocation.getCurrentPosition(success, error);
});
Avoid
// Requesting immediately on DOMContentLoaded — intrusive
document.addEventListener('DOMContentLoaded', () => {
    navigator.geolocation.getCurrentPosition(success, error);
});

Avoid requesting notification permission on page load

Warning
notification-on-start

Notification prompts shown on load are dismissed by nearly all users. Ask after demonstrating value.

Requesting push notification permission immediately on page load results in 90%+ rejection rates and trains users to deny all notifications. Request after demonstrating value — e.g. after a successful action.

Recommended
// Request after user opts in
document.getElementById('enable-notifications').addEventListener('click', () => {
    Notification.requestPermission();
});
Avoid
// Requesting on page load — dismissed by almost all users
window.addEventListener('load', () => Notification.requestPermission());

Allow users to paste into password fields

Warning
password-inputs-can-be-pasted-into

Blocking paste prevents password manager use and encourages weaker passwords.

Blocking paste on password fields prevents password manager use, forces manual typing of complex passwords, and drives users toward weaker passwords. Never prevent paste — it reduces security, not increases it.

Recommended
<!-- Standard password input — paste is allowed by default -->
<input type="password" name="password" autocomplete="current-password">
Avoid
<!-- Never do this -->
<input type="password" onpaste="return false;">

Images have correct aspect ratio

Info
image-aspect-ratio

Ensure images are displayed at their natural aspect ratio to prevent distortion.

When the CSS display size has a different aspect ratio than the intrinsic image size, the image appears distorted. Setting `width` and `height` attributes and using `aspect-ratio` CSS prevents this.

Recommended
<!-- Intrinsic 16:9 image displayed at 16:9 -->
<img src="video-thumb.jpg" width="800" height="450" alt="Video">

/* CSS */
img { aspect-ratio: 16/9; width: 100%; height: auto; }
Avoid
<!-- 16:9 image displayed in a square container — distorted -->
<img src="video-thumb.jpg" style="width:200px; height:200px;">

Cache the Laravel configuration

Warning
config-cache-disabled

Run `php artisan config:cache` in production deploys.

Without `php artisan config:cache`, Laravel parses every config file on every request. The cached version is loaded once and stays in OPcache.

Typical savings: 5-15ms per request
Recommended
# In your deploy script
php artisan config:cache

Cache the route table

Warning
route-cache-disabled

Run `php artisan route:cache` in production deploys.

Route registration is one of Laravel's slowest boot operations. Caching pre-compiles all routes into a single file.

Typical savings: 10-30ms per request on apps with 100+ routes
Recommended
# In your deploy script
php artisan route:cache

Pre-compile Blade views

Info
view-cache-disabled

Run `php artisan view:cache` to avoid runtime compilation.

Pre-compiling Blade views avoids per-request compilation. Views are still compiled on first render, but `view:cache` warms them up at deploy time.

Recommended
# In your deploy script
php artisan view:cache

Disable APP_DEBUG in production

Critical
debug-on-prod

Debug mode leaks stack traces and slows requests.

APP_DEBUG=true exposes internal stack traces (database credentials, file paths) on errors AND inflates response time by collecting debug data.

Recommended
# .env in production
APP_DEBUG=false
APP_ENV=production
Avoid
# .env in production — leaks stack traces!
APP_DEBUG=true

Enable OPcache

Warning
opcache-disabled

OPcache caches the compiled PHP bytecode and is essential for production performance.

Without OPcache, PHP recompiles every script on every request. With OPcache, compiled bytecode is cached in shared memory — typically 2-3x speedup on the application layer.

Recommended
; In production php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0

Pin a PHP version

Info
missing-php-version

Add an explicit php constraint in composer.json so deploys reject incompatible versions.

An explicit PHP constraint in composer.json prevents `composer install` from succeeding on incompatible production servers — fail fast at deploy time, not in runtime.

Recommended
{
  "require": {
    "php": "^8.2"
  }
}

Switch session driver from file

Info
session-driver-file

Use redis or database for sessions on multi-process production hosts.

File-based sessions create lock contention and disk I/O on every request. Multi-process production hosts (PHP-FPM with > 1 worker) need a centralised store like Redis.

Recommended
# .env
SESSION_DRIVER=redis
SESSION_CONNECTION=default
Avoid
# .env (single-server only)
SESSION_DRIVER=file

Switch cache driver from file

Info
cache-driver-file

Use redis or memcached in production.

File cache reads/writes hit disk on every operation. Redis or Memcached operate from RAM, typically 10-100x faster.

Recommended
# .env
CACHE_STORE=redis

Configure a real queue connection

Warning
queue-driver-sync-prod

Sync queue runs jobs in-process and blocks responses. Use redis/database/sqs in production.

Sync queue runs jobs inline within the HTTP request, blocking the response until the job completes. Defeats the purpose of queueing.

Recommended
# .env
QUEUE_CONNECTION=redis
Avoid
# .env (dev-only)
QUEUE_CONNECTION=sync

Avoid excessive DOM size

Warning
excessive-dom-size

Page has :count DOM elements. Large DOMs slow rendering and JS interactions. Aim for under 1500 elements.

Large DOMs (>1500 nodes) slow style recalculations and layout, causing jank during scroll and interactions. Often caused by deeply nested templates or rendering huge lists without virtualization.

Recommended
// Paginate or virtualize large lists
$items = $query->paginate(50);
Avoid
// Rendering 5000 items at once
@foreach ($thousandsOfItems as $item)
  <div>...</div>
@endforeach

Improve cache policy

Info
cache-policy-short

:count resource(s) have a TTL under 30 days. Long-term caching reduces repeat-visit load times.

Short cache TTL (< 30 days) on static assets forces repeat downloads on return visits. Long TTL combined with hashed filenames (Vite default) is safe and cuts repeat-visit load times to near zero.

Recommended
# nginx — long cache for hashed assets
location ~* \.(js|css|webp|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

Use HTTP/2 (or HTTP/3)

Info
uses-http2

HTTP/2 multiplexes requests over a single connection — faster than HTTP/1.1 for resource-heavy pages. Most modern hosts (Forge, Vapor, Cloudflare) enable it by default.

HTTP/1.1 establishes one connection per resource. HTTP/2 multiplexes many requests over a single connection, drastically reducing handshake overhead. Most managed Laravel hosts (Forge, Vapor, Cloudflare) enable HTTP/2 by default — but a misconfigured custom server may not.

Recommended
# nginx — enable HTTP/2
listen 443 ssl http2;
ssl_certificate /path/to/cert.pem;

Asset filenames are not content-hashed

Warning
assets-not-hashed

Without content hashes (`app-Df8gK3p2.js`), you cannot cache assets aggressively — every change requires invalidation. Vite generates hashed filenames by default; verify your build output.

Hashed asset names (`app-Df8gK3p2.js`) let browsers cache assets forever — when content changes, the hash changes, the URL changes, the browser fetches the new version. Without hashes, you cannot use long cache TTLs without serving stale content. Vite's default config produces hashed filenames automatically.

Recommended
{{-- Blade layout — Vite handles hashed filenames automatically --}}
@vite(['resources/css/app.css', 'resources/js/app.js'])
Avoid
{{-- Hardcoded asset paths bypass Vite's hashing --}}
<script src="/build/app.js"></script>

seo

5 items

Add a meta description

Warning
meta-description

Search engines display the meta description in result snippets.

Search engines display meta descriptions in result snippets. A clear 50-160 char description significantly improves click-through rate.

Recommended
<meta name="description" content="Buy authentic Nike Air Max sneakers — fast shipping, free returns.">

Document has a valid hreflang

Warning
hreflang

Use correct BCP47 hreflang values so search engines show the right language version.

For multilingual sites, `hreflang` tells search engines which page to show for which language and region. Incorrect or missing `hreflang` leads to the wrong language version appearing in search results.

Recommended
<link rel="alternate" hreflang="en" href="https://example.com/en/page">
<link rel="alternate" hreflang="fr" href="https://example.com/fr/page">
<link rel="alternate" hreflang="x-default" href="https://example.com/page">
Avoid
<!-- Missing hreflang — search engines guess the target language -->
<!-- Or invalid language code -->
<link rel="alternate" hreflang="english" href="..."><!-- invalid BCP47 -->

Document has a canonical link

Warning
canonical

Add <link rel="canonical"> to declare the authoritative URL and prevent duplicate content indexing.

Duplicate content across URLs (with/without trailing slash, www vs non-www, UTM parameters) confuses search engines and dilutes PageRank. A canonical tag tells Google which is the authoritative URL.

Recommended
<!-- In Blade layout -->
<link rel="canonical" href="{{ url()->current() }}">

<!-- Or with Laravel SEO packages -->
// In AppServiceProvider: URL::forceRootUrl(config('app.url'));
Avoid
<!-- No canonical — search engines may index many duplicate versions -->
<!-- /page, /page/, /page?utm_source=twitter all treated as separate URLs -->

robots.txt is valid

Warning
robots-txt

An invalid or missing robots.txt can block search engines from crawling your site.

An invalid or missing robots.txt can prevent search engines from crawling your site, or allow crawling of sensitive paths. Lighthouse flags when robots.txt is unreachable or syntactically invalid.

Recommended
# public/robots.txt — sensible defaults
User-agent: *
Disallow: /admin/
Disallow: /api/
Sitemap: https://example.com/sitemap.xml
Avoid
# Accidentally blocking all crawlers
User-agent: *
Disallow: /

# Or missing robots.txt — crawlers get no guidance

Tap targets are sized appropriately

Warning
tap-targets

Touch targets should be at least 48×48 CSS pixels to be easily tappable on mobile.

Touch targets smaller than 48×48 CSS pixels are hard to tap accurately on mobile. Users mis-tap neighbouring elements, triggering unintended actions. Google's mobile-friendliness criteria require adequate tap target sizes.

Improves Google mobile-friendliness score and reduces accidental tap errors
Recommended
/* Minimum 48×48px tap target with padding trick */
.nav-link {
    display: inline-flex;
    align-items: center;
    min-height: 48px;
    padding: 0 12px;
}

/* Or use Tailwind */
<a class="py-3 px-4 min-h-12">...</a>
Avoid
/* Too small — hard to tap on mobile */
.icon-btn { width: 20px; height: 20px; }
Open Vitals Spotlight (Cmd+K)
Open Vitals Spotlight (Ctrl+K)
Type at least 2 characters to search
↑↓ Navigate Open
0 results