Looking to hire Laravel developers? Try LaraJobs

laravel-vitals maintained by corentinbtmps

Description
GTMetrix-style Lighthouse audits combined with backend telemetry and host-app source pointers, for Laravel 11/12/13.
Last update
2026/05/22 12:41 (dev-main)
License
Links
Downloads
0

Comments
comments powered by Disqus

CI Latest Stable Version License

Laravel Vitals

Performance auditing, Real User Monitoring, and actionable recommendations — all inside your Laravel app.


Table of contents


What is Laravel Vitals

Laravel Vitals runs Google Lighthouse against your own pages, captures what your server was doing at that exact moment — queries, memory, N+1 problems — and points directly at the lines of code responsible. Everything lands in a dashboard at /vitals that any team member can read and act on. Real User Monitoring is opt-in with one Blade directive. Your data stays in your own database — no SaaS, no per-seat billing.


Why Laravel Vitals

Capability GTMetrix / PageSpeed Laravel Vitals
Lighthouse scores (Performance, Accessibility, SEO, Best Practices)
Backend telemetry — queries, memory, N+1, cache
Source code references — exact file and line in your app
Real User Monitoring — self-hosted, no PII paid / ✗
Performance budgets with CI exit codes
GitHub PR auto-comments with score table
Self-hosted — your data stays yours

Requirements

  • PHP 8.2 or higher
  • Laravel 11, 12, or 13
  • Livewire 4 and Flux Free 2 (installed automatically by Composer)
  • Local driver: Node 18+ and npm install -g lighthouse
  • Playwright driver: Node 18+ and npm install playwright playwright-lighthouse in your project
  • PageSpeed driver: a free Google PageSpeed Insights API key

The auto driver (default) tries localplaywrightpagespeed in order. If your server has Node and the lighthouse CLI, no extra setup is needed.


Installation

composer require corentinbtmps/laravel-vitals
php artisan vendor:publish --tag=vitals-config
php artisan migrate
php artisan vitals:install

Add @vitalsRum to your main layout's <head> to enable Real User Monitoring:

<head>
    <meta charset="utf-8">
    @vitalsRum
</head>

Verify the setup:

php artisan vitals:doctor

Quick start

Declare a URL in config/vitals.php:

'urls' => ['home' => '/'],

Run your first audit:

php artisan vitals:audit home --sync

Visit /vitals.

That's it. You now have a Lighthouse score, backend telemetry, and a list of recommendations with file/line references in your own source code.


Architecture

  1. Declare URLs in config/vitals.php as 'label' => '/path' pairs. Run vitals:audit manually, on a schedule, or from CI.
  2. Audit starts. The package signs an X-Vitals-Audit-Id header with your APP_KEY and spawns Lighthouse (local Node, Playwright, or Google PageSpeed API depending on your driver).
  3. Backend telemetry. While Lighthouse navigates the page, your own middleware detects the signed header and records: query count, total query time, N+1 suspicion, peak memory, views rendered, jobs dispatched, and cache hits/misses.
  4. Lighthouse result. Scores arrive (0–100) alongside raw metric values: LCP, INP, CLS, TTFB, FCP, TBT, Speed Index.
  5. Code analysis. Static analyzers scan your Blade views, Vite config, and composer.json to attach exact file:line references to each Lighthouse finding. Everything is stored and surfaced at /vitals. If a budget threshold is exceeded or a regression is detected, a Slack message or email fires automatically.

Features

SEO checks (22 Google-aligned)

Laravel Vitals ships its own SEO check engine that runs alongside every Lighthouse audit. Unlike Lighthouse's built-in SEO score (which covers ~8 signals), the custom checks cover 22 areas — every one a Google-documented ranking signal, no Yoast-style heuristics:

Category Checks
Configuration (3) noindex directive, nofollow, robots.txt indexability
Content (5) H1 uniqueness, HTTPS resources, image alt text, broken links (samples ≤ 30), broken images
Meta (7) Meta description, title length, Open Graph image, HTML lang, canonical URL, JSON-LD structured data, invalid head elements
Performance (7) TTFB ≤ 600ms, HTTP status code, HTML size, image size, JS bundle size, CSS size, gzip/Brotli compression

Results appear on /vitals/audits/{id}/seo (per-audit deep view with actual vs expected values, hint text, and links to Google documentation) and on the /vitals/seo cross-URL page (aggregated score table, top failing checks by category, period filter).

The combined vitals_seo_score blends Lighthouse's SEO score (50%) with the weighted pass rate of all custom checks (50%), producing a stricter 0–100 score.

// config/vitals.php
'seo' => [
    'enabled'         => env('VITALS_SEO_ENABLED', true),
    'thresholds'      => ['title_max_chars' => 60, 'ttfb_ms' => 600],
    'disabled_checks' => [],  // e.g. ['broken-links', 'css-size']
],

Lighthouse audits — three drivers

Lighthouse simulates a page load under realistic mobile conditions and scores Performance, Accessibility, Best Practices, and SEO from 0 to 100.

Driver Requires Best for
local Node 18+ + lighthouse CLI Dev machines and CI with Node
playwright Node 18+ + playwright + playwright-lighthouse Docker-based CI
pagespeed Google API key (VITALS_PAGESPEED_API_KEY) Public URLs, no Node needed
auto Falls back through the above Default — works in most environments

The local and playwright drivers work behind authentication. The pagespeed driver requires a publicly reachable URL and cannot capture backend telemetry.

php artisan vitals:audit home --driver=local --device=mobile
php artisan vitals:audit --all --sync   # all URLs, synchronous (CI-friendly)

Backend telemetry

When Lighthouse loads a page, the package captures a server-side snapshot: query count, total query time, unique queries, N+1 suspect flag (triggered when queries_count / queries_unique ≥ 10), peak memory, views rendered, jobs dispatched, cache hits/misses, and any slow queries that exceed the configured threshold (default: 50ms).

When N+1 queries are detected, the package also stores up to 200 normalized SQL patterns per request with the exact PHP file and line that triggered each query (skipping vendor and package frames to always point at your own code). The audit detail and issue detail pages surface the top 3 repeated patterns with occurrence counts and caller locations.

This answers questions that Lighthouse alone cannot: "Why is our LCP slow — is it the database?" or "Which exact line in my controller is causing 42 repeated queries?"

By default, telemetry is only captured during audit runs — zero overhead for real visitors. Set telemetry.always_capture = true to sample a configurable percentage of all requests (default 5%), similar to how Laravel Pulse works.


Source code references

This is the key differentiator. When Lighthouse flags "render-blocking resources" or "unused JavaScript", the package shows you the exact file and line in your codebase that caused it.

Recommendation: Eliminate render-blocking resources
Source: resources/views/layouts/app.blade.php  line 12
  <link rel="stylesheet" href="/css/bootstrap.min.css">
  Hint: Use @vite([...]) to bundle and version this asset, or add defer/async.

Seven analyzers scan your project: Blade asset tags, image tags, Laravel config settings (missing config:cache, debug mode in production), Composer packages, Vite config, Blade view patterns, and .env settings.


Real User Monitoring

Add @vitalsRum to your layout's <head> to collect Core Web Vitals (LCP, INP, CLS, TTFB, FCP) from real visitors. Beacons are sent via sendBeacon after page load — no impact on performance. Results appear at /vitals/rum.

Privacy by design: no IP addresses, no cookies, no session identifiers. Only metric values, URL path, device type, connection hint, and user-agent are stored. See Privacy for the full list.


Database query baselines

The /vitals/queries page answers a question most teams cannot: "Did this recent deployment slow down the database on route X?"

For each route with telemetry data you get the number of samples, Typical (75th percentile) and Worst case (95th percentile) for both query count and query time. Routes are compared to the previous equivalent period — if you are looking at 7 days, it compares to the 7 days before that. A route is flagged with a regression badge when its current Typical query count is more than double the baseline. Any route whose audits have triggered N+1 detection gets a red N+1 badge inline with its name.

Click any route to expand a per-route panel below the table with:

  • Affected URLs — every URL that hit this route in the period, linking back to URL detail.
  • Recent audits — last five audits with this route name, with their queries count and time.
  • Repeated SQL patterns — the top SQL patterns the N+1 detector caught on this route, with occurrence count and the exact file:line caller (clickable, opens in your IDE).

A "Memory-hungry routes" panel below ranks the top 5 routes by typical peak memory — useful for catching routes that spike to 256 MB on 25% of requests before they cause production incidents.


Performance budgets

Set limits on any metric. When an audit exceeds a limit, an alert fires.

// config/vitals.php
'budgets' => [
    'lcp_ms'            => ['warning' => 2500, 'critical' => 4000],
    'score_performance' => ['warning' => 90,   'critical' => 70],
    // ... per_url overrides available
],

Use --fail-on-budget in CI to get non-zero exit codes (1 = warning, 2 = critical):

php artisan vitals:audit --all --sync --fail-on-budget --format=junit > results.xml

Dashboard

A Livewire application at /vitals — no asset compilation required in your app.

Eight top-level pages: Overview · URLs · Issues · SEO · RUM · Queries · Learn · Budgets. A built-in Cmd+K spotlight searches across URLs, audits, and recommendations anywhere in the dashboard.

Access control: available in local by default. To allow access in production, define the viewVitals gate:

use LaravelVitals\Facades\Vitals;

Vitals::authorize(fn ($user) => $user?->is_admin ?? false);

Issues page

/vitals/issues has two tabs. Top issues shows cross-URL quick wins, URLs that are currently worsening or improving, and third-party script cost breakdowns. All recommendations aggregates every recommendation across all audits, sorted by frequency — making it easy to find the one fix that would improve the most pages at once.

Each row is a severity-tinted card: amber for warning, rose for critical, sky for info. The title link inherits the card's severity color so the page reads at a glance — no need to parse small badges. Click any title (or the trailing N occurrences button) to land on /vitals/issues/{audit_key} — a deep view that shows every occurrence grouped by URL, with the exact file and line where the issue originates in your code. For N+1 queries, this page lists the top 3 repeated SQL patterns with occurrence counts and caller location.

Each recommendation includes severity (info / warning / critical), category (Performance, Accessibility, Best Practices, SEO), the source code reference (file + line), a one-sentence fix hint, and links to the relevant web.dev article and Laravel documentation. The package covers Lighthouse findings, Laravel-specific issues (missing config:cache, debug mode in production, sync queue, disabled OPcache), and backend signals (N+1 queries with SQL attribution, slow queries, large payloads).


Learn knowledge base

/vitals/learn is a browsable knowledge base of every detectable issue, grouped by category (Performance, Accessibility, Best Practices, SEO). Each entry covers what causes the finding, its impact, and how to fix it — with links to web.dev and the relevant Laravel documentation, plus side-by-side Recommended / Avoid code snippets when relevant. The category tiles at the top double as filters and show the active-in-your-app count per category, so you can spot which class of issues currently bites your project hardest.


JSON API

A read-only JSON API at /vitals/api/v1, protected by the same viewVitals gate. No separate API tokens required — the session cookie is sufficient for CI scripts making authenticated requests. Intended for custom dashboards, CI scripts, and third-party integrations.

Endpoint Returns
GET /vitals/api/v1/audits Paginated audit list
GET /vitals/api/v1/audits/{id} Single audit with scores, metrics, recommendations
GET /vitals/api/v1/urls All configured URLs
GET /vitals/api/v1/urls/{id}/latest Most recent audit for one URL
GET /vitals/api/v1/recommendations All stored recommendations

Query parameters: ?page=N&per_page=M (default 25, max 100), ?since=YYYY-MM-DD&until=YYYY-MM-DD.

curl -s https://yourapp.com/vitals/api/v1/urls/1/latest \
  -H "Accept: application/json"

CI integration

GitHub Action — audits your preview deployment and posts a score comparison table as a PR comment. Your team sees the performance impact before merging, without running any manual checks.

# .github/workflows/pr-perf.yml
- uses: corentinbtmps/laravel-vitals/.github/actions/vitals-pr-comment@v1.0.0
  with:
    preview-url: ${{ vars.PREVIEW_URL }}
    base-url: https://your-production-app.com
    github-token: ${{ secrets.GITHUB_TOKEN }}
    fail-on-regression: 'true'  # exit 1 when score drops > regression-threshold

Available inputs: preview-url (required), base-url, github-token (required), fail-on-regression (default false), regression-threshold (default 5 points), devices (default mobile; accepts mobile,desktop).

Pre-commit hook — runs vitals:doctor before every git commit and blocks the commit if any check fails (missing migration, broken asset, mis-configured notification):

php artisan vitals:install-hook             # install (or --type=pre-push for push hook)
php artisan vitals:install-hook --uninstall # remove (restores previous hook from backup)

Slack notifications

Set VITALS_NOTIFICATIONS_SLACK_WEBHOOK in .env. Each completed audit that triggers an alert posts a message to your channel. Budget violations and regression alerts are posted as replies in the same thread — the Slack message timestamp is stored so follow-ups always find the right thread.

VITALS_NOTIFICATIONS_SLACK_WEBHOOK=https://hooks.slack.com/services/...
VITALS_NOTIFICATIONS_MAIL_TO=team@yourcompany.com

Notification triggers: budget_violation (on by default), regression (on, 10% threshold), weekly_digest (on), audit_completed (off — too noisy for most teams). Supported channels: mail, slack, database.

Add these to your scheduler:

Schedule::command('vitals:digest:send')->weekly();
Schedule::command('vitals:check-regressions')->daily();

Public endpoints

Health checkGET /vitals/health returns database status, driver availability, queue status, and telemetry buffer state. HTTP 200 = all checks pass; 503 = something failed. No auth required — suitable for Uptime Robot and similar monitors.

Status pageGET /vitals/status is an opt-in public page showing uptime %, Core Web Vitals distribution, and recent incidents. Enable it in config/vitals.php:

'status' => ['enabled' => true, 'title' => 'My App Status'],

Self-monitoringvitals:self-check reports Vitals table sizes and the 10 slowest telemetry captures. Schedule it hourly to catch overhead early.


Boost and Claude Code integration

php artisan vitals:install writes two context files into your project:

  • .ai/guidelines/vitals.blade.php — used by Laravel Boost and compatible AI tools
  • .claude/skills/laravel-vitals/SKILL.md — used by Claude Code (Anthropic's CLI)

These help AI coding assistants generate correct code when you ask them to add recommendations or RUM metrics. Re-publish after updating the package with php artisan vitals:boost:install --force.


Configuration

Full annotated source: config/vitals.php. The most useful keys:

Key Default What it controls
driver auto Lighthouse driver. auto tries local → playwright → pagespeed.
urls [] Monitored URLs as ['label' => '/path'].
telemetry.always_capture false Set true to sample all requests, not just audits.
telemetry.slow_query_threshold_ms 50 Queries slower than this are stored individually.
budgets.* see config Warning/critical thresholds per metric. per_url allows per-URL overrides.
rum.sample_rate 1.0 Fraction of page loads that send a RUM beacon (0.05–0.1 for high-traffic).
notifications.channels ['mail'] Add 'slack' or 'database' as needed.
ui.editor null Editor preset — see "Open in editor" section below.
ui.editor_url_template null Custom URL scheme override — see "Open in editor" section below.
status.enabled false Opt-in public status page at /vitals/status.
retention.days 90 Audit and RUM retention. RUM has its own rum.retention_days (also 90 days).

The UI is available in English, French, German, and Spanish — follows app()->getLocale().

Open in editor

Add VITALS_EDITOR=vscode (or phpstorm, cursor, sublime, atom, textmate, idea, zed, nova, macvim, emacs) to your .env and every file:line reference in the dashboard becomes a clickable link that jumps to the right file and line in your IDE — the same UX as Spatie Ignition's error pages.

For a custom URL scheme not in the preset list, set VITALS_EDITOR_URL_TEMPLATE directly: myeditor://{path}:{line}. The custom template overrides the preset.

Future versions may auto-detect the editor from the request context.


Artisan commands

Command What it does
vitals:audit {label} Audit one URL
vitals:audit --all Audit all URLs (queued batch)
vitals:audit --all --sync Audit all URLs synchronously
vitals:demo Seed fictional data for exploration
vitals:doctor Run diagnostic checks
vitals:install Publish Boost and Claude Code skill files
vitals:install-hook Install a git pre-commit hook
vitals:discover --routes List candidate URLs from your routes
vitals:discover --sitemap=URL List candidate URLs from a sitemap
vitals:check-regressions Compare latest audits to the 7-day baseline
vitals:digest:send Send a weekly summary
vitals:purge --demo Remove demo data
vitals:purge Remove all Vitals data (confirmation required)
vitals:boost:install Re-publish Boost / Claude skill files
vitals:boost:diff Check if installed AI files differ from package
vitals:self-check Check table sizes and slow telemetry captures

Key vitals:audit options:

Option Description
--device=mobile|desktop Device profile (default: mobile)
--driver=local|playwright|pagespeed Override configured driver
--sync Run synchronously instead of queuing
--fail-on-budget Exit 1 (warning) or 2 (critical) on budget violations
--format=table|json|junit Output format

Privacy

What RUM collects: metric name and value, rating (good/needs-improvement/poor), URL path, device type, connection hint, navigation type, user-agent string.

What RUM does NOT collect: IP addresses, cookies, session identifiers, user IDs, geolocation, or any fingerprint.

Backend telemetry records server-side performance metrics, not user behavior. It is only stored during audit runs unless telemetry.always_capture is enabled.

Retention: audits and RUM events default to 90 days. Configure via retention.days and rum.retention_days. Schedule model:prune daily to apply it:

$schedule->command('model:prune', ['--model' => [
    \LaravelVitals\Models\Audit::class,
    \LaravelVitals\Models\Recommendation::class,
    \LaravelVitals\Models\BackendTelemetry::class,
    \LaravelVitals\Models\RumEvent::class,
]])->daily();

Because no PII is collected in RUM beacons, this data typically does not require GDPR consent mechanisms. Consult your legal team for your jurisdiction.


Performance impact

Middleware: CaptureVitalsTelemetry returns immediately on every request that does not carry the X-Vitals-Audit-Id header. The fast-path overhead is sub-microsecond.

RUM script: ~4.25 kB gzipped, loaded with defer, sends beacons only after full page load via sendBeacon.

Dashboard assets: served by dedicated package routes with long-lived cache headers. Never loaded for non-dashboard routes.


Troubleshooting

1. "URL [home] not found in config or database" Declare it in config/vitals.php first: 'urls' => ['home' => '/'], then re-run the audit.

2. Lighthouse fails with a Chrome error in Docker / Linux Add --no-sandbox to chrome_flags:

'drivers' => ['local' => ['chrome_flags' => ['--headless', '--no-sandbox']]],

3. Dashboard shows "Access Denied" Define the viewVitals gate in AppServiceProvider:

Vitals::authorize(fn ($user) => $user?->is_admin ?? false);

4. RUM data not appearing Verify @vitalsRum is in <head> and VITALS_RUM_ENABLED=true is set. Check the browser network tab for a POST to /vitals/rum/ingest.

5. vitals:doctor shows failing checks Read the output — each line includes a remediation hint. Common fixes: php artisan migrate (unpublished migrations), Add VITALS_NOTIFICATIONS_MAIL_TO (mail notifications configured without a recipient).


Contributing

See CONTRIBUTING.md for setup instructions, code conventions, and how to add a new recommendation or RUM metric.


License

Laravel Vitals is open-source software released under the MIT license.

Built with: Livewire, Flux, Google Lighthouse, web-vitals, spatie/laravel-searchable, spatie/laravel-onboard.