Real browser metrics → causal degradation graph → actionable report. Built for AI agents, useful for humans.
PerfGraph launches a headless Chromium browser, captures performance data via Chrome DevTools Protocol, runs it through a 5-stage analysis pipeline, and produces a structured report with issues sorted by severity, causal chains, and prioritized fixes.
npx perfgraph run --url https://example.com --prettyPerfGraph — not a symptom list, a diagnosis.
Root cause → impact → fix, in one graph.
Lighthouse gives you a score. PerfGraph tells you why it's bad and what to fix first.
Instead of digging through a 10k-line trace.json or a wall of Lighthouse audits, you get a focused report with root causes linked to impact. The output is designed to be read by AI agents (or you) without a decoder ring.
collect → normalize → extract → analyze → report
| Step | What it does |
|---|---|
| Collect | Launches headless Chromium, captures network, trace, performance API, JS coverage, console logs, DOM snapshot, and Lighthouse audit |
| Normalize | Validates raw data through Zod schemas into a unified Intermediate Representation (IRBundle). All timestamps normalized, data cleaned |
| Extract | Computes 7 diagnostic feature sets from the IR: LCP breakdown, critical network chain, main-thread blocking time, JS hotspots, layout shifts, third-party overhead, render-blocking score |
| Analyze | Runs 30+ causal rules against the features, builds a directed graph where edges represent known causal relationships with confidence levels (strong/medium/weak) |
| Report | Produces a structured report (TOON for agents, Markdown for humans) with issues sorted by severity, causal chains from root cause to user impact, and prioritized recommendations with expected impact estimates |
npm install -g perfgraphOr skip the install:
npx perfgraph --helpRequirements: Node.js ≥ 22 and Playwright's Chromium build. The browser is not downloaded automatically — install it once:
npx playwright install chromiumIf the binary is missing, PerfGraph fails with the exact expected path instead of a cryptic launch error.
Full pipeline, one command:
perfgraph run --url https://example.com --prettyWant to use PerfGraph from your AI agent? See AI agent workflow.
Step by step:
# 1. Collect data
perfgraph collect --url https://example.com --output ./results
# 2. Normalize → Extract → Report
perfgraph normalize ./results/perfgraph_example_20260609_120000 --output ir.json
perfgraph extract ir.json --output features.json
perfgraph report features.json --output report.json --prettyFull pipeline in one shot.
--url <url> Required. Target URL to analyze.
--output <dir> Output directory (default: ./perfgraph-output).
--runs <n> Number of collection runs (default: 1).
--pretty Pretty-print the final report JSON.
--device <name> Mobile emulation (e.g. "iPhone 13").
--owned-domain <host> Treat an extra hostname as first-party CDN (repeatable).
--no-lighthouse Skip Lighthouse collection.
--no-coverage Skip JS/CSS coverage.
--no-console Skip console log capture.
--no-dom Skip DOM snapshot.
Captures performance data from a URL. See run options — same flags apply.
Converts raw collected data into a validated IRBundle. Accepts a run directory or a parent directory (auto-detects latest run).
--output, -o <file> Write to file (default: stdout).
--pretty Pretty-print JSON.
Computes diagnostic features from a normalized IR bundle.
--input, -i <file> Path to IR JSON (alternative to positional).
--output, -o <file> Write to file (default: stdout).
--pretty Pretty-print JSON.
Applies causal rules and builds a degradation graph.
--input, -i <file> Path to FeatureSet JSON (alternative to positional).
--output, -o <file> Write to file (default: stdout).
--pretty Pretty-print JSON.
Generates the final performance report. Accepts a FeatureSet JSON (from extract) — runs the causal engine internally, no need to call analyze separately.
--input, -i <file> Path to FeatureSet JSON (alternative to positional).
--output, -o <file> Write to file (default: stdout).
--pretty Pretty-print JSON.
npm install -g perfgraph
npx playwright install chromiumThe package ships an agent skill that describes how to invoke the CLI and how to interpret its artifacts. Copy it into your agent's skills directory:
# OpenCode
cp -r node_modules/perfgraph/skills/perfgraph ~/.config/opencode/skills/
# Claude Code
cp -r node_modules/perfgraph/skills/perfgraph ~/.claude/skills/If you work on the skill itself, link it instead of copying it. A copy goes stale, and then the agent learns the old way to run the tool:
# Linux and macOS
ln -s "$PWD/node_modules/perfgraph/skills/perfgraph" ~/.config/opencode/skills/perfgraphIn a checkout of this repository the skill lives at
skills/perfgraph/SKILL.md, with the metric
reference in skills/perfgraph/references/interpretation.md.
perfgraph run --url https://example.com --output ./perfgraph-output --prettyRead the resulting files progressively:
manifest.toon— files actually produced;insights.toon— compact triage and top recommendations;report.toon— issues, causal chains, evidence and recommendations;report.mdorgraph.toon— readable diagnosis and causal graph;- raw
trace.json,network.jsonandlighthouse.jsononly for targeted deep dives.
The agent should summarize the status, show an ASCII causal degradation graph
for the 1–3 main chains, give 3–5 prioritized actions, and mention confidence,
warnings and missing data. .toon files are text and should not be parsed as
JSON. The full agent instructions live in
skills/perfgraph/SKILL.md.
perfgraph run --runs 3 writes runs-summary.json next to the run
directories. It holds the metrics of every run, both the lab run PerfGraph
measured and the Lighthouse run, and the spread of each: variance for the
lab run — the run the report describes — and varianceLighthouse for
Lighthouse's own runs. The first run is often an outlier, so the median is the
number to quote.
insights.toon is the compact triage artifact. It keeps the two measurement
runs apart, because PerfGraph and Lighthouse each navigate the page on their
own:
runContext: browser, os, device, viewport, throttling, owned domains
metrics: lcpMs, fcpMs, cls, tbtMs — measured in the PerfGraph run
lighthouse: performance, lcpMs, fcpMs, cls, tbtMs, failedAudits — Lighthouse's own run
lcpElement: selector, nodeLabel, source, type
documentResponse: status, headers of the document — cache status, encoding, alt-svc
metrics.lcpSource names the run the LCP came from. lcpElement.source is
performance-api when the browser's own LCP entry identified the element,
lighthouse or trace otherwise; lcpElement.type is text or image, and
only an image LCP has a resource URL and load timings.
documentResponse.headers answers what the timings cannot: whether the HTML
is cached at the edge (cf-cache-status), whether it may be cached at all
(cache-control), how it is encoded (content-encoding) and whether a
faster protocol is offered (alt-svc).
criticalPath.documentChain appears when the browser loaded more than one
document — it navigated away from a page it had already received.
wastedServerMs is the server time spent on a response nobody used,
contentAtMs when the content the visitor saw arrived.
perfgraph run writes report.toon (compact, agent-facing) and
report.md (human-readable) next to the raw artifacts; the same report object
is printed as JSON on stdout. Key sections:
| Category | Issues |
|---|---|
| LCP | Slow LCP, high TTFB, render-blocking resources, LCP resource delay chains |
| JavaScript | Long tasks, high TBT, heavy execution, unused code, main-thread bottlenecks |
| Network | Deep request chains, bandwidth bottlenecks, waterfall depth, document chains |
| Layout | Layout shifts (CLS), large DOM size, forced reflows |
| Third-party | Third-party script overhead, tracking pixels, embedded widget impact |
| Lighthouse | Forced reflows with the source, oversized images, uncached bytes, duplicated and legacy JavaScript, blocking fonts |
The Lighthouse insight audits become findings of their own, carrying the value Lighthouse measured. They are marked as coming from the Lighthouse run, not from the traced one.
src/
├── index.ts CLI entry — lazy-loads commands
├── collect/ CDP data collection via Playwright
│ ├── session.ts Browser launcher + CDP session
│ ├── orchestrator.ts Collection orchestrator
│ ├── coverage.ts JS/CSS coverage
│ ├── network.ts Network request capture
│ ├── performance.ts Performance API metrics
│ ├── runtime.ts Runtime metadata
│ ├── dom.ts DOM snapshot
│ └── lighthouse.ts Lighthouse audit
├── normalize/ Data normalization & IR validation
├── extract/ 7 diagnostic feature extractors
├── causal/ Causal rule engine (30+ rules)
│ ├── builder.ts Graph construction
│ ├── rules/ Individual causal rules by category
│ └── types.ts Graph data types
├── report/ Report generation & scoring
│ ├── analyzer.ts Report builder
│ ├── scorer.ts Score computation
│ ├── remediations.ts Remediation templates
│ └── types.ts Report schema
├── distill/ Agent-optimized summary layer (insights.toon)
├── output/ TOON/JSON writers, manifest
├── cli/ CLI command handlers
└── shared/ Shared utilities & types
skills/perfgraph/ Agent skill + interpretation reference
scripts/ Build helpers
spec/ Feature specs
npm install
npm run build # compile TypeScript
npm run typecheck # check types only
npm test # run tests
npm run test:watch # tests in watch mode
npm run dev # tsx watch — no build step| Thing | What |
|---|---|
| Runtime | Node.js ≥ 22 |
| Language | TypeScript (strict, noUncheckedIndexedAccess) |
| Browser | Playwright (Chromium) |
| Validation | Zod at every data boundary |
| Graph engine | @dagrejs/graphlib |
| Performance audits | Lighthouse |
| Testing | Vitest |
MIT
{ "meta": { "url": "https://example.com", "analyzedAt": "2026-06-09T19:15:19.000Z", "reportVersion": "1.3.0", "featureCount": 7, "graphNodeCount": 24, "graphEdgeCount": 31, "ruleCount": 32, }, "summary": { "score": "moderate", // "good" | "moderate" | "poor" "criticalIssues": 2, "warnings": 5, "infos": 3, "topIssues": [ { "id": "js-long-task", "label": "Long task", "severity": "critical", "confidence": "strong", }, ], }, "issues": [ { "id": "lcp-slow", "label": "LCP exceeds 2.5s threshold", "severity": "critical", "value": 4320, "unit": "ms", "threshold": 2500, "confidence": "strong", "basis": "observed", "findingStatus": "confirmed", "remediation": "Optimize largest contentful paint element...", "chainId": "lcp:3", }, ], "chains": [ { "id": "lcp:3", "rootCause": "LCP > 2.5s", "impact": "Poor user experience", "confidence": "strong", "severity": "critical", "path": [ { "nodeId": "ttfb:1", "label": "TTFB delayed by server response", "type": "root-cause", "basis": "observed", "findingStatus": "confirmed", "impactSurface": "page", }, { "nodeId": "render-blocking:2", "label": "Render-blocking stylesheets", "type": "bottleneck", "basis": "inferred", "findingStatus": "suspected", "impactSurface": "lcp", }, ], "length": 2, }, ], "recommendations": [ { "priority": "critical", "category": "LCP", "title": "Optimize Largest Contentful Paint", "description": "LCP element is delayed by the render-blocking chain.", "action": "Inline critical styles, defer non-critical CSS", "expectedImpact": "Reduces LCP by ~40%", "expectedImpactStatus": "estimated", "applicability": "applicable", "verification": "Re-run perfgraph and compare insights.toon LCP", "relatedIssues": ["lcp-slow"], }, ], "features": { /* raw extracted features for cross-referencing */ }, }