Skip to main content

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.

FieldTypeDefaultControls
buildDirstring.nextWhere boot-graph/imports/chunks read the build manifest and chunk files from.
srcDirstringsrcThe project source root, used to resolve bootEntry and aliases for imports.
bootEntrystring[]['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.
routesstring[]['/']Which routes measure/analyze hit. cpu, boot-graph, and chunks all target routes[0] only.
serverUrlstringhttp://localhost:3000Base URL for measure, cpu, and analyze's server-reachability check.
measureRunsnumber6Default Lighthouse run count for measure, if not overridden by the [runs] argument or analyze --runs. Aim for ≥6 for a stable floor.
cpuSettleMsnumber4000How 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).
aliasesRecord<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 entriesLibrary 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'
FunctionSignatureReturns
loadConfig(cwd?)(cwd?: string) => PerfConfigThe 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)
}