Profiler
Recording
The two ways to capture a profile — profile a page by URL, or profile a single request with a header — plus curated JSON, viewing a profile on serverless via dump and upload, the production secret, sharper names with source maps, and every option.
On this page
There are two ways to capture a profile. The main one lives on the dashboard at /__profiler — type a path and profile it; the other is a header you can send from anywhere.
Profile a page
Type a path, and the profiler renders it several times back to back and profiles just those renders. This is the main way in — reach for it whenever you know which page is slow, which is almost always.
It does one un-profiled warm-up render first, so the one-time cost of loading modules and filling caches lands outside the measured window and the median stays steady.
/__profiler → Profile a page → /some/slow/pageThe page renders through your real server, so anything it needs — a session, cookies, a load — runs exactly as in production.
Profile a single request
Send any request with the header x-profile: <secret> and get a profile of exactly that request. The report URL comes back in the x-profile-report response header.
curl -sD - 'https://example.com/slow/page' -H 'x-profile: your-secret' | grep x-profile-reportThis starts a fresh CPU profile for the one request, so it adds the profiler’s start-up cost to that request’s latency. Use it to catch a specific slow request, not for steady measurement.
For agents and scripts
Every report is also available as curated JSON — not the raw V8 profile, but the analyzed result: self/total per component, network attribution, memory, GC, and the same findings the human report shows.
# an existing report
GET /__profiler/report/<id>.json
# profile a page and get the JSON back in one request (no redirect, no HTML)
GET /__profiler/page?p=/slow/page&runs=6&format=json&key=<secret>Sending Accept: application/json to /record or /page does the same. The payload is self-describing (schema, version, and units), so an agent can read it directly:
{
"schema": "ogygia-profiler-report",
"summary": { "window_ms": 699, "busy_pct": 100, "verdict": "compute-bound" },
"findings": [{ "severity": "warn", "code": "sequential-network", "message": "…" }],
"components": [{ "name": "PrimeSieve", "file": "src/…/PrimeSieve.svelte", "self_ms": 296, "total_ms": 296 }],
"network": { "sequential_ms": 0, "hosts": [], "calls": [] },
"memory": { "growth_mb": 195, "gc": { "max_ms": 2.2 }, "allocators": [] }
// budget, hot_functions, files, requests, links…
}Serverless and edge
The live dashboard wants a long-lived Node server: the V8 inspector, and memory that survives between requests. Serverless Node platforms (Lambda, Amplify) can record — the inspector runs in-process — but the report lives in memory on an instance that may be gone before you can browse to it. True edge runtimes (Workers) have no inspector at all, so there is nothing to record beyond the always-on request log.
For the serverless case, take the report with you. Any report downloads as a self-contained JSON dump — everything needed to render it, no inspector required to read it back:
# a one-shot page profile, straight to a dump
GET /__profiler/page?p=/slow/page&runs=6&format=dump&key=<secret>
# or an existing report
GET /__profiler/report/<id>/dumpThen open /__profiler/view anywhere — your production route, or the same app on localhost — and upload the file. The full HTML report renders from the dump alone, because the renderer is a pure function of it.
In production
Set a secret. Without one, the UI is disabled and 404s.
PROFILER_SECRET=some-long-random-stringThen reach the dashboard with ?key=<secret>. The key is remembered in a cookie for that session, so links inside a report work without repeating it. Matching is timing-safe.
Sharper names with source maps
Production bundles rename and inline code, so some frames show as (anonymous) or point at a chunk file. Build with server source maps and the profiler maps every frame back to its source file and recovers the original names:
// vite.config.ts
export default defineConfig({
build: process.env.PROFILER_SOURCEMAPS ? { sourcemap: true } : undefined,
// ...plugins
});Then build with the flag set:
PROFILER_SOURCEMAPS=1 vite buildThe report header shows sourcemapped when it worked. Leave it off for normal builds; turn it on when you are hunting.
Cost and safety
- Idle cost is almost nothing. The CPU is only sampled while recording (~1–3% during a recording). Between recordings there is no sampling.
- Always-on work per request is wall timing, a capped in-memory request log, and — unless you turn network capture off — one
AsyncLocalStoragewrap plus afetch/httppatch to attribute outbound calls. Pass{ network: false }for the leanest path. Server-Timingheaders expose internal timings to every client, so they are on in dev, off in production by default. Pass{ serverTiming: true }to force them on.- Recorded profiles are kept in memory only (the last few, gzipped), never written to disk.
Options
profiler({
secret: process.env.PROFILER_SECRET, // default: the PROFILER_SECRET env var
path: '/__profiler', // UI base path
sampleInterval: 500, // µs between CPU samples
maxReports: 6, // profiles kept in memory (gzipped)
network: true, // patch fetch/http to attribute calls
serverTiming: undefined, // default: on in dev, off in production
heap: true, // sample heap allocations while recording
enabled: true, // master switch
});