Command & Config Reference
Commands
codeleap-perf boot-graph # heavy libraries on the boot bundle (expect CLEAN) — static
codeleap-perf imports # third-party packages on the static boot import graph — static
codeleap-perf chunks # per-chunk sizes + duplication of configured markers — static
codeleap-perf measure [runs] [--headless] # TBT/LCP/FCP, floor of N runs (default 6) — needs server; headed by default
codeleap-perf cpu # boot CPU self-time by library — needs server
codeleap-perf analyze [options] # the lot + a verdict; exits 1 on FAIL
--no-server skip measure/cpu even if a server is up (pure static run)
--cpu also run the CPU profile (slower, fuzzier than measure)
--headless drive the internal measure call headless (default is headed)
--runs N Lighthouse runs per route for measure (default config.measureRuns)
--json [file] write the full report to disk (default perf-report.json)
Each command's real job and its exact output are covered below. The three static ones share one
concept: the boot bundle is the union of the /_app and / entries in Next's
build-manifest.json — the JS that loads on every page before anything else runs.
boot-graph — is a heavy library reachable on boot?
Scans every boot chunk for the SDK-internal markers configured in markers (see
Configuration). This is the fastest signal and the one to run after any change
to _app, a shared hook, or an import you're not sure is lazy. The boot bundle is the union of
the pages-router /_app entry and config.routes[0]'s entry in Next's build manifest — change
routes[0] if the route you care about isn't the first one configured.
$ codeleap-perf boot-graph
boot-graph — 22 boot chunks, 1387KB total JS
✓ CLEAN — no heavy library on the boot graph
With a hit:
boot-graph — 19 boot chunks, 1463KB total JS
⚠ Sentry in static/chunks/0mtm-t~x.v~qi.js
✗ 1 hit(s) — heavy code is on boot
imports — what third-party code is on the static boot graph?
Walks the static import/export … from graph from bootEntry, following only local files
(relative imports and configured aliases). Anything it can't resolve locally is a third-party
package. This is pure text analysis — no module ever executes — so it's safe to run without any
build step or stubs, and it answers a different question than boot-graph: not "is this specific
heavy library present" but "what's on the graph at all."
$ codeleap-perf imports
imports — 64 files on the boot static graph; third-party:
@codeleap/form (1 file)
@codeleap/web (6 files)
@tanstack/react-query (2 files)
next (7 files)
react (5 files)
chunks — sizes and cross-chunk duplication
Lists every boot chunk by size (largest first) and re-runs the same markers scan boot-graph
uses, but reports something different: a marker present in two or more chunks, which usually
means a bundler failed to dedupe a module that's imported from two different places (a page and
_app, most commonly). See Improving Boot Performance
for the specific pattern this catches.
$ codeleap-perf chunks
chunks — 22 boot chunks, 1387KB total
189KB static/chunks/0mtm-t~x.v~qi.js
144KB static/chunks/185qrih9f73.f.js
116KB static/chunks/0_x-jie70-nou.js
...
duplicated across chunks:
⚠ useSentryUser in 2 chunks
measure — real TBT/LCP/FCP
Drives Lighthouse directly through its Node API and chrome-launcher (no npx/PATH dependency),
using the default Slow-4G + 4× CPU throttling — the same profile a heavy real-world device
experiences, and what actually surfaces TBT. Runs N times per configured route (default 6) and
reports the floor and median of each metric.
$ codeleap-perf measure 8
run 1: LCP=3321ms FCP=914ms TBT=104ms CLS=0.003 score=92
run 2: LCP=4498ms FCP=912ms TBT=93ms CLS=0.003 score=84
...
measure — http://localhost:3000 · compare by FLOOR (median in parens)
/ score 84 · TBT 92 (96) · LCP 3321 (4496) · FCP 910 (912) · CLS 0.003 ✓
[runs] (positional) overrides config.measureRuns for this call. Launches a real, visible
Chrome window by default (alias flag --visible, opt out with --headless) — TBT and FCP read
about the same either way, but headless LCP has been observed to read artificially low (e.g.
~2100–2500ms) against a real, visible browser measuring the same build closer to 4s — a real gap
between the two modes, not just noise. Only pass --headless when no display is available (e.g. a
CI runner without Xvfb); never mix the two modes within one comparison.
cpu — boot CPU self-time by library
Drives headless Chrome over the DevTools protocol, profiles the first route in routes at 4×
CPU throttle, and attributes self-time to a library by fingerprinting each chunk's source (no
source maps required). This is a secondary, exploratory signal, not a decision-making one — CPU
self-time is not the same thing as TBT, since a function can cost real self-time while running
entirely outside the >50ms blocking window. Use it to compare two builds' composition; confirm
any real TBT impact with measure's floor (the subtract method — see
Measuring Correctly).
$ codeleap-perf cpu
cpu — boot self-time @ http://localhost:3000/ · 4× throttle
scripted 294ms · idle/gc 4006ms
by library (chunk fingerprint + styles-engine fns):
103ms next/turbopack-runtime (35%)
57ms next (19%)
37ms (app/other) (13%)
16ms firebase (5%)
analyze — everything, plus a verdict
Runs all three static checks always; adds measure (and cpu, with --cpu) when a server is
reachable at config.serverUrl; exits with code 1 if the verdict fails, so CI can gate a merge on
it. The internal measure call runs headed by default, for the same reason measure does on its
own (see above) — pass --headless if no display is available.
$ codeleap-perf analyze
run 1: LCP=4504ms FCP=913ms TBT=103ms CLS=0.003 score=84
run 2: LCP=4510ms FCP=909ms TBT=99ms CLS=0.003 score=84
...
═══ perf report · 2026-07-06T12:54:50.451Z · load 3.36/2.95/2.76 ═══
boot-graph ✓ CLEAN — 22 chunks, 1387KB
chunks 1387KB total, 22 chunks, largest 189KB
imports 19 third-party on boot (64 files)
measure / score 84 · TBT 90 · LCP 3925⚠ · FCP 909 · CLS 0.003 (floor of 6)
─── VERDICT: ✗ FAIL (1 issue(s)) ───
⚠ /: LCP floor 3925 > 2200
The header records machine load — a number without its conditions is not comparable to any other
number, including a past run of your own project. ⚠ marks a metric over its configured
threshold; the verdict fails on any boot-graph hit or over-threshold metric. --json [file]
writes the same report to disk (default perf-report.json) as a durable baseline anchor.
Configuration
Sensible defaults target a Next.js pages-router app; most projects need no config at all. To
override, drop a codeleap-perf.config.json at the project root:
{
"buildDir": ".next",
"srcDir": "src",
"bootEntry": ["src/pages/_app.tsx", "src/pages/_document.tsx"],
"aliases": { "@/": "src/" },
"routes": ["/", "/auth"],
"serverUrl": "http://localhost:3000",
"measureRuns": 6,
"cpuSettleMs": 4000,
"thresholds": { "tbt": 100, "lcp": 2200, "fcp": 1100 },
"markers": [{ "name": "Zod", "pattern": "safeParse|ZodError" }],
"sigs": [
{ "name": "react-aria", "pattern": "react-aria|useFocusRing|VisuallyHidden|@react-aria" },
{ "name": "Sentry", "pattern": "sentry", "flags": "gi" }
]
}
You only need to set the fields you're overriding — anything omitted keeps its default (shown
below). Setting markers or sigs replaces the whole default list rather than merging into it;
every other field merges shallowly (or, for thresholds/aliases, key-by-key) over the defaults.
| Field | Type | Default | Controls |
|---|---|---|---|
buildDir | string | .next | Where boot-graph/imports/chunks read the build manifest and chunk files from. |
srcDir | string | src | The project source root, used to resolve bootEntry and aliases for imports. |
bootEntry | string[] | ['src/pages/_app.tsx', 'src/pages/_document.tsx'] | The entry files whose static import graph imports walks — change this for a non-pages-router layout. |
routes | string[] | ['/'] | Which routes measure/analyze hit. cpu, boot-graph, and chunks all target routes[0] only. |
serverUrl | string | http://localhost:3000 | Base URL for measure, cpu, and analyze's server-reachability check. |
measureRuns | number | 6 | Default Lighthouse run count for measure, if not overridden by the [runs] argument or analyze --runs. Aim for ≥6 for a stable floor. |
cpuSettleMs | number | 4000 | How long cpu waits after the page's load event before stopping the CPU profile — long enough to capture post-load work (e.g. a deferred SDK init), short enough not to pad the profile with idle time. |
thresholds | { tbt, lcp, fcp } | { tbt: 100, lcp: 2200, fcp: 1100 } | The PASS/FAIL cutoffs measure/analyze compare the floor against (milliseconds). |
aliases | Record<string, string> | { '@/': 'src/' } | Import-alias → path-prefix map, so imports can resolve an aliased import as a local file instead of reporting it as third-party. |
markers | { name, pattern }[] | 8 common heavy libraries (Zod, react-aria, lottie, crop, dropzone, Sentry, firebase, ua-parser) | Regexes boot-graph and chunks scan chunk source for. See below. |
sigs | { name, pattern, flags? }[] | 9 built-in entries | Library fingerprints the cpu command uses to attribute each chunk's source to a library name. flags is a RegExp flags string; defaults to 'g' (use 'gi' for case-insensitive). Replaced wholesale when provided — not merged. |
markers drives both boot-graph (any hit anywhere is a fail) and chunks (a hit in ≥2 boot
chunks is duplication). pattern must be an SDK-internal symbol regex, never a package-name
substring — a package name appears verbatim in the import('...') call-site strings that made it
lazy, so a substring match false-positives on the thing you already fixed. Markers aren't
restricted to third-party SDKs — you can add a first-party symbol (e.g. a shared hook's name) to
catch duplication automatically; a marker added this way will still trigger boot-graph's
single legitimate occurrence as a "hit," which is expected, not a regression.
sigs is only used by the cpu command — it controls how cpu labels each boot chunk's source
in the profile output. Unlike markers, entries don't need to be SDK-internal symbols; their
purpose is attribution (naming a chunk), not leak detection. pattern is a RegExp source string;
flags defaults to 'g' (pass 'gi' for case-insensitive). Setting sigs replaces the whole
default list rather than merging into it.
Import DEFAULT_CONFIG from the package if you want to inspect or extend the current defaults
from code rather than JSON:
import { DEFAULT_CONFIG } from '@codeleap/perf'
console.log(DEFAULT_CONFIG.markers)
Programmatic API
Every analyzer the CLI uses is also exported directly, each returning the same data the CLI prints from — for a custom CI step, a dashboard, or a one-off script:
import {
analyze, bootGraph, traceImports, analyzeChunks, measure, cpuProfile,
loadConfig, DEFAULT_CONFIG,
} from '@codeleap/perf'
| Function | Signature | Returns |
|---|---|---|
loadConfig(cwd?) | (cwd?: string) => PerfConfig | The resolved config — codeleap-perf.config.json merged over DEFAULT_CONFIG, or the defaults untouched if no config file exists. |
bootGraph(config) | (config: PerfConfig) => BootGraphResult | { bootChunks: number, totalBootKb: number, hits: { marker: string, chunk: string }[], clean: boolean } |
traceImports(config) | (config: PerfConfig) => ImportsResult | { files: number, thirdParty: { pkg: string, files: number }[] } |
analyzeChunks(config) | (config: PerfConfig) => ChunksResult | { totalKb: number, count: number, chunks: { chunk: string, kb: number }[], duplicated: { name: string, chunks: number }[] } |
measure(config, runs?, opts?) | (config: PerfConfig, runs?: number, opts?: { headed?: boolean }) => Promise<RouteMeasure[]> | One entry per configured route: { route: string, url: string, runs: number, score: number, tbt: { floor, median }, lcp: { floor, median }, fcp: { floor, median }, cls: number, over: string[] } |
cpuProfile(config) | (config: PerfConfig) => Promise<CpuResult> | { url: string, scriptedMs: number, idleMs: number, byLibrary: { lib, ms, pct }[], topChunks: { chunk, ms, lib }[] } |
analyze(config, opts?) | (config: PerfConfig, opts?: AnalyzeOptions) => Promise<AnalyzeReport> | { timestamp: string, loadavg: number[], serverUp: boolean, bootGraph, imports, chunks, measure: RouteMeasure[] | null, cpu: CpuResult | null, verdict: { pass: boolean, issues: string[] } } |
bootGraph, traceImports, and analyzeChunks are synchronous and read only the build output —
no server needed. measure, cpuProfile, and analyze are async and need a running server for
their non-static parts; analyze itself never throws for "no server," it just sets
serverUp: false and leaves measure/cpu null.
A minimal CI gate:
import { analyze, loadConfig } from '@codeleap/perf'
const config = loadConfig()
const report = await analyze(config, { cpu: true })
if (!report.verdict.pass) {
console.error(report.verdict.issues.join('\n'))
process.exit(1)
}