Boot trace

The module graph can overlay how long every provider and controller took to construct during one real boot. A slow constructor or onModuleInit stops being a guess and becomes a bar with a number on it.

Why it needs a change to your code

nestjs-doctor is a scanner that reads source files. It never runs your application, so construction time does not exist for it to measure.

NestJS measures it already. Booting with { snapshot: true } records an initTime for every class and assembles a SerializedGraph. Writing that graph to disk takes one change to main.ts, and --timings reads it back.

Let an agent do it

The change below is mechanical, and an agent can make it:

npx nestjs-doctor@latest --init

That installs a nestjs-boot-trace skill. Ask for a boot trace and the agent edits main.ts, boots once, runs the scan, reads the cascade, and puts main.ts back. It works in Claude Code, Cursor, Codex, and the other agents on Coding agents.

The rest of this page is the same work by hand.

Capture the dump

Two versions gate what you get:

  • Class construction times: @nestjs/core 9.3.9 or newer, which added initTime.
  • Lifecycle hook times: Nest 11.1.4 or newer, which added the instrument option.

Pick your version. The right column is what src/main.ts becomes, and it is development-only either way:

main.ts
1-import { NestFactory } from "@nestjs/core";
2import { AppModule } from "./app.module";
3
4-const app = await NestFactory.create(AppModule);
5await app.listen(3000);
main.ts, instrumented
1+import { writeFileSync } from "node:fs";
2+import { performance } from "node:perf_hooks";
3+import { NestFactory, SerializedGraph } from "@nestjs/core";
4import { AppModule } from "./app.module";
5
6+const t0 = performance.now();
7+const app = await NestFactory.create(AppModule, { snapshot: true });
8+const createMs = performance.now() - t0;
9+await app.init();
10+const initMs = performance.now() - t0;
11await app.listen(3000);
12+const startupMs = performance.now() - t0;
13+
14+const graph = JSON.parse(app.get(SerializedGraph).toString());
15+Object.assign(graph, { createMs, initMs, startupMs });
16+writeFileSync("nestjs-doctor-timings.json", JSON.stringify(graph));

snapshot: true is the part that makes NestJS record initTime. The three performance.now() markers become the lifecycle strip. On 11.1.4 and newer, instanceDecorator wraps each instance's hooks so their durations land in hookTimings.

That decorator replaces a method on every instance in the application. Keep it out of production behind an environment check or a separate entry point.

Boot the app once, then scan:

npx nestjs-doctor@latest . --report --timings nestjs-doctor-timings.json

Relative paths resolve against the scanned directory. Without --report the flag is ignored, with a warning.

Read a time

Each class's time includes waiting on its own dependencies. A shared slow dependency therefore counts again in every class that awaits it.

Read down a cascade until the number drops. The class where it drops owns the time. If UsersService reads 120ms and the SlowService it injects reads 119ms, SlowService owns it.

A module node shows its slowest single class, never a sum.

What the report shows

ElementWhereMeans
time to start ≈ <ms>Header badgeBootstrap start until app.listen() resolved, hooks included, from startupMs. Without it, falls back to boot ≈ <ms>, the slowest construction chain
Lifecycle stripTop of the Boot trace tabcreate · lifecycle hooks · listen, from the createMs and initMs markers
Proportional barsPer class, slowest firstColored by type: provider, controller, injectable, middleware
Amber segmentOn a class barThe share not explained by that class's slowest dependency, so roughly its own work
+120ms init chipOn a trace rowA hookTimings entry, joined only when the class name is unique in the dump. Repeated entries from transient providers merge into one ×N chip
reused tagDimmed, hollow rowThe dependency was already built when its parent loaded, so its cost was paid at its first consumer
listed above tagOn a repeatThe dependency has its own top-level row, so its cost is not drawn twice

Either tag carries down to the rows nested under it. Selecting a module fills the Boot trace tab, which shares the bottom dock with Module problems. A trace button next to the module's name in the detail panel opens the same tab.

Limits

  • Timings are display-only. They never affect the score, the diagnostics, or the exit code.
  • An unreadable, malformed, or unrecognized dump degrades to a stderr warning, and the report renders without timings.
  • Out-of-order phase markers drop the lifecycle strip, also with a warning.
  • A timing attaches to a module only when that module's class name is unique, inside the dump and across a monorepo's projects.