Find the main-thread stalls hiding in a Chrome performance trace.
janktrace reads DevTools trace JSON as a stream, selects the renderer main
thread, and reports long tasks plus the work that tends to cause them. It has no
runtime dependencies and does not load the whole trace into memory.
Example output from the test fixture:
janktrace small-trace.json
main thread CrRendererMain (pid 1, tid 10)
trace span 179.0 ms · 8 complete events · 3 tasks
long tasks 2 at or above 50.00 ms · 140.0 ms blocked
worst task 80.00 ms
top stalls
80.00 ms at +19.00 ms RunTask
60.00 ms at +119.0 ms RunTask
Node 20 or newer is required.
git clone https://github.com/Denoax/janktrace.git
cd janktrace
npm install
node ./bin/janktrace.js path/to/profile.trace.jsonGzipped traces and stdin work too:
janktrace profile.trace.json.gz
cat profile.trace.json | janktrace -Set a different long-task threshold, emit JSON, or enforce a budget in CI:
janktrace --threshold 40 --top 12 profile.trace.json
janktrace --json profile.trace.json > report.json
janktrace --budget 100 profile.trace.json # exits 1 if the worst task is over 100 msRecord a trace in Chrome DevTools under Performance, then use Save
profile. Traces recorded through Puppeteer or the Chrome DevTools Protocol
work when they contain complete (ph: "X") timeline events.
import { createReadStream } from 'node:fs';
import { analyzeTrace, parseTraceEvents } from 'janktrace';
const events = parseTraceEvents(createReadStream('profile.trace.json'));
const report = await analyzeTrace(events, { thresholdMs: 50, top: 8 });parseTraceEvents() also accepts a raw array of trace events. A single event is
limited to 8 MiB by default so malformed input cannot quietly consume the
machine. Override maxEventBytes only for traces that genuinely contain larger
events.
- A task is a complete event named
RunTaskor one of Chromium's equivalent task-runner names. - A long task lasts at least
--thresholdmilliseconds. The default is 50 ms. - Activity totals are inclusive durations for recognized scripting, rendering, painting, and garbage-collection events. Nested work can overlap, so the totals should not be added together.
- If thread metadata is missing, the busiest thread containing task events is used and the report says so.
This is a trace summary, not a replacement for the DevTools flame chart. It does not attribute every long task to a call frame, interpret async events, or claim that all time over a budget was visible jank. Chromium's trace vocabulary also changes; unknown events are intentionally ignored.
Performance traces become large quickly. The parser scans only far enough to
find the top-level traceEvents array, buffers one event, and hands that event
to the analyzer. JSON.parse() still handles each event, which keeps the small
parser concerned with boundaries rather than reimplementing JSON.
npm test
npm run checkTests cover hostile chunk boundaries, escaped strings, malformed input, thread selection, reporting, and CLI exit codes.
MIT