perf-reviewer
v1.0.0
Published
Lightweight frontend performance review SDK — collect runtime signals and generate developer-friendly reports
Downloads
164
Maintainers
Readme
perf-reviewer
A lightweight, focused frontend performance review SDK. Drop it into any web app, collect runtime performance signals, and get a developer-friendly report explaining what looks slow, risky, or worth investigating — without any backend, dashboard, or external service required.
Why this exists
Browser DevTools and Lighthouse are great for manual audits, but they can't tell you what happens in production with real users and real data. perf-reviewer runs inside your app at runtime, observes actual browser signals (Web Vitals, resource timing, long tasks, API durations), and gives you a structured performance report you can inspect, log, or export on demand.
It is not Sentry. It is not a dashboard. It is a developer-focused tool for understanding runtime performance while building and debugging.
Installation
npm install perf-reviewerQuick start
import { createPerformanceReviewer } from 'perf-reviewer'
const reviewer = createPerformanceReviewer({
appName: 'shop-web',
environment: 'development',
})
reviewer.start()
// Track an API call
await reviewer.trackApiCall('load_products', () => fetch('/api/products'))
// Measure an expensive section
await reviewer.measure('render_cart', async () => {
await renderCart()
})
const report = reviewer.generateReport()
console.log(report)API
createPerformanceReviewer(config)
Creates a new reviewer instance.
const reviewer = createPerformanceReviewer({
appName: 'my-app', // required
environment: 'production', // optional
release: '2.3.0', // optional
sessionId: 'custom-id', // optional — auto-generated if omitted
thresholds: { // optional — override any threshold
slowApiCallMs: 800,
componentVerySlowRenderMs: 100,
},
})reviewer.start()
Begins collection. Captures navigation timing, starts observing resource timing entries and long tasks, and registers Web Vitals callbacks.
reviewer.start()Call this once, early in your app lifecycle (e.g. in main.ts or _app.tsx).
reviewer.stop()
Disconnects PerformanceObservers. Web Vitals callbacks remain active until the page is unloaded. Calling start() after stop() is not supported — use a new reviewer instance or reset().
reviewer.stop()reviewer.measure(name, fn, metadata?)
Times an async function and records the result.
const data = await reviewer.measure('fetch_and_render', async () => {
const res = await fetch('/api/data')
return res.json()
}, { route: '/checkout' })- Returns the original result of
fn - Records
success: trueon completion,success: falseon thrown errors - Rethrows the original error unchanged
reviewer.trackApiCall(name, fetcher, metadata?)
Wraps a fetch-returning function and records timing, HTTP status, and success.
const response = await reviewer.trackApiCall('load_products', () =>
fetch('/api/products')
)fetchermust returnPromise<Response>- Returns the original
Response - Records
success: response.ok - If
fetcherthrows, records a failed call and rethrows the original error
reviewer.recordComponentRender(name, durationMs, metadata?)
Records a component render duration manually. Designed for use in custom React Profiler adapters or framework instrumentation.
reviewer.recordComponentRender('ProductGrid', 42, { itemCount: 120 })reviewer.generateReport()
Produces a PerformanceReport snapshot from all collected data.
const report = reviewer.generateReport()The report is a plain JSON-serializable object. See Report structure below.
reviewer.reset()
Clears all collected records (API calls, measurements, component renders, Web Vitals, navigation timing, resource timing, long tasks) and resets the running state.
reviewer.reset()
reviewer.start() // safe to start fresh after resetReport structure
generateReport() returns a PerformanceReport:
{
appName: string
environment?: string
release?: string
sessionId: string
generatedAt: number // Unix timestamp (ms)
score: number // 0–100
rating: 'good' | 'needs-improvement' | 'poor'
summary: string[] // 2–5 human-readable summary lines
webVitals: {
cls?: MetricReview
lcp?: MetricReview
inp?: MetricReview
fcp?: MetricReview
ttfb?: MetricReview
}
navigation?: {
domContentLoadedMs?: number
loadEventMs?: number
responseTimeMs?: number
transferSize?: number
}
apiCalls: {
total: number
failed: number
slow: number
verySlow: number
averageDurationMs?: number
slowest: ApiCallRecord[] // top 5, sorted by duration desc
failures: ApiCallRecord[]
}
measurements: {
total: number
failed: number
slow: number
verySlow: number
averageDurationMs?: number
slowest: MeasurementRecord[] // top 5
failures: MeasurementRecord[]
}
components: {
total: number
slow: number
verySlow: number
slowest: ComponentRenderRecord[] // top 5
byName: Array<{
name: string
renderCount: number
averageDurationMs: number
maxDurationMs: number
}>
}
resources: {
totalTracked: number // capped at 20 (slowest kept)
slow: number
verySlow: number
slowest: ResourceTimingRecord[]
}
longTasks: {
totalTracked: number // capped at 20 (slowest kept)
slowest: LongTaskRecord[]
}
recommendations: string[]
}Each MetricReview:
{
value: number
rating: 'good' | 'needs-improvement' | 'poor'
description: string // human-readable explanation
}Score algorithm
The score starts at 100 and deductions are applied:
| Signal | Per item | Cap | |---|---|---| | Poor Web Vital | −15 | — | | Needs-improvement Web Vital | −7 | — | | Failed API call | −5 | −20 | | Slow API call | −3 | −15 | | Very slow API call | −6 | −24 | | Failed measurement | −4 | −16 | | Slow measurement | −2 | −10 | | Very slow measurement | −5 | −20 | | Slow component render | −2 | −10 | | Very slow component render | −5 | −20 | | Long task | −3 | −15 | | Very slow resource | −3 | −15 |
Score is clamped to [0, 100].
| Score | Rating |
|---|---|
| ≥ 90 | good |
| 60–89 | needs-improvement |
| < 60 | poor |
Default thresholds
| Threshold | Default |
|---|---|
| slowApiCallMs | 1000 ms |
| verySlowApiCallMs | 2500 ms |
| slowMeasureMs | 100 ms |
| verySlowMeasureMs | 500 ms |
| slowResourceMs | 1000 ms |
| verySlowResourceMs | 2500 ms |
| longTaskMs | 50 ms |
| lcpGoodMs | 2500 ms |
| lcpNeedsImprovementMs | 4000 ms |
| inpGoodMs | 200 ms |
| inpNeedsImprovementMs | 500 ms |
| clsGood | 0.1 |
| clsNeedsImprovement | 0.25 |
| ttfbGoodMs | 800 ms |
| ttfbNeedsImprovementMs | 1800 ms |
| componentSlowRenderMs | 16 ms |
| componentVerySlowRenderMs | 50 ms |
Override any subset via the thresholds config option.
Limitations
- Not Lighthouse. Lighthouse audits static resources and simulated load.
perf-reviewerobserves real-time browser signals during actual usage — different data, different purpose. - Not Sentry. There is no error tracking, user session replay, or backend reporting. This is purely a local, in-memory signal collector.
- Recommendations are hints, not root causes. The library observes signal patterns and surfaces likely areas to investigate. It cannot know why something is slow without deeper profiling.
- Component tracking is manual. There is no automatic React/Vue/Svelte integration yet. You call
recordComponentRender()yourself, or build a thin adapter on top. - Web Vitals are async. LCP and CLS are reported lazily by the browser. Calling
generateReport()immediately afterstart()may show no vitals yet. - SSR-safe. The SDK checks for browser API availability before every call and fails silently in server-side rendering contexts.
Roadmap
- React Profiler adapter (
usePerformanceReviewer) - Automatic
fetchinstrumentation (opt-in, no monkey-patching by default) report.toHTML()— render report as a readable HTML page- Browser console pretty-printer (
reviewer.printReport()) - JSON export / import for comparing reports across sessions
- Web Vitals attribution (element references, selector hints)
- Historical comparison between two
PerformanceReportsnapshots - CI-friendly browser test example (Playwright + perf-reviewer)
License
MIT
