A V8 deoptimization toolkit for JavaScript and TypeScript on Node.js β visibility into inline cache states, deoptimizations, hidden-class (map) churn, and CPU profile data, packaged for AI agents and everyday dev workflows.
deoptkit began as a rebuild of Microsoft's Deopt Explorer VSCode extension, re-targeted at AI agents instead of humans and updated for modern V8 (tested against V8 14 / Node 26; the original supported V8 8β9). Where the extension offered tree views and editor decorations to browse, deoptkit answers the question agents actually ask: "what should I fix first?" β with ranked findings, explanations, suggested fixes, and a first-class before/after comparison to verify the fix worked.
Background reading: the TypeScript team's Introducing Deopt Explorer article, where this class of analysis produced an 8β10% compiler speedup.
deoptkit ships several surfaces over one engine, all from the deoptkit package:
| Surface | What it is |
|---|---|
MCP server (deoptkit / deoptkit mcp) |
Eleven tools + a prompt that let an AI agent profile a workload, get ranked findings, drill in, and verify fixes. |
deoptkit ci |
Snapshot-style regression gate: fails CI when a change introduces new structural findings (megamorphic ICs, deopt loops, map churn). |
deoptkit lsp |
A language server that publishes findings as inline editor diagnostics (squiggles + Problems panel). |
deoptkit/harness |
mark() / observed() β plant in-log window markers and warm up hot code in benchmark scripts. |
deoptkit/vitest + deoptkit/bench |
A vitest bench preset that records V8 logs per worker, and benchObserved() to window each case. |
deoptkit/serve |
driveServer() β boot an SSR app in-process and drive it with requests so its runtime is observable. |
library API (deoptkit) |
parseLog, computeFindings, compareSessions, runWorkload, β¦ for building your own tooling. |
Register the stdio server with your MCP host. For Claude Code, add to .mcp.json in your project (or run claude mcp add):
{
"mcpServers": {
"deopt": {
"command": "npx",
"args": ["-y", "deoptkit"]
}
}
}For VSCode (Copilot Chat / MCP-capable extensions), add to .vscode/mcp.json:
{
"servers": {
"deopt": { "command": "npx", "args": ["-y", "deoptkit"] }
}
}The server advertises instructions describing the workflow, so a connected agent knows what to do without reading this file. The loop:
profile_run { command: ["node", "bench.js"] }β runs the workload under V8 logging flags (no flag knowledge needed) and loads the result as a session.get_findings { sessionId }β everything wrong, ranked worst-first with severity 1β100, source locations, V8-level explanations, and suggested-fix categories.- Fix the top finding β at its
originalposition when present (that's the source you wrote), otherwise its file/line. profile_runagain, thencompare_sessions { baseSessionId, headSessionId }β reports the findings your change resolved, any regressions it introduced, and per-function CPU deltas.
Drill-down tools when a finding needs more context: get_function (annotated source + its ICs/deopts/ticks), get_map (a hidden class's transition chain and which call sites it polluted), list_ics, list_deopts, list_functions, load_log (analyze an existing v8.log), load_manifest (load every log from a vitest bench run), list_sessions.
Warm-up matters. V8 only optimizes hot code, so profile a workload that runs the target thousands of times β the
observed()helper below encodes this. For libraries, profile the built output, not source: bundling changes shapes and inlining.
// bench/parse.bench.mjs
import { observed } from "deoptkit/harness";
import { parseRecord } from "../dist/index.mjs";
const inputs = makeVariedInputs(); // realistic, varied shapes β not homogeneous
observed("parse record", (i) => parseRecord(inputs[i % inputs.length]), {
iterations: 50_000
});Then profile_run { command: ["node", "bench/parse.bench.mjs"] } and window the findings with fromMark: "parse_record_start", toMark: "parse_record_end".
Schema-driven inputs (Valibot + Valimock) are the easiest way to get realistically varied shapes β see docs/recipes.md.
// vite.config.ts
import { defineConfig } from "vitest/config";
import { deoptKit } from "deoptkit/vitest";
export default defineConfig({
test: { ...deoptKit({ outDir: ".deopt" }) }
});// src/__benchmarks__/parse.bench.ts
import { benchObserved } from "deoptkit/bench";
import { parseRecord } from "../parse";
benchObserved("parse record", () => parseRecord(next()));Run vitest bench, then load_manifest { path: ".deopt/manifest.json" } to load every worker's log as a session. (Coverage must be off β the preset refuses to run otherwise, since instrumentation changes optimization behavior.)
Full recipes, including SSR apps (Next/Astro via deoptkit/serve) and builds, live in docs/recipes.md.
deoptkit ci (and the vitest workflow) write .deopt/findings.json. Point the language server at it to get deopt squiggles in your editor:
deoptkit lsp --findings .deopt/findings.jsonThe server watches the file and republishes on every change, so vitest bench --watch β re-run ci β squiggles update live. It publishes diagnostics at source-mapped original positions with the explanation and suggested fix in the hover; severity maps from the finding score (error β₯ 60, warning β₯ 25, info below), and findings older than the file they annotate downgrade to info with a re-run hint.
Editor wiring. The server speaks standard LSP (initialize + push-model publishDiagnostics), so any editor with a generic LSP client works today:
- Neovim:
vim.lsp.start({ name = "deoptkit", cmd = { "npx", "deoptkit", "lsp" }, root_dir = vim.fn.getcwd() }) - Helix / Zed: register a language server with command
npx deoptkit lspand attach it tojavascript/typescript.
VSCode has no built-in generic LSP client, so a dedicated deoptkit extension is the tracked next step (BENCHMARKING.md Β§8, v1.4). Until then, VSCode + Claude Code users get the same findings through the MCP server β the agent reads them via get_findings and reports them in chat, which needs no editor setup at all.
deoptkit ci bench/parse.bench.mjs # first run writes .deopt/baselines/β¦, passes
deoptkit ci bench/parse.bench.mjs # exit 1 only on NEW structural findings
deoptkit ci --update bench/parse.bench.mjs # accept current findings as the baselineBaselines contain identity-only structural findings (no line numbers, severities, or timing), so they survive unrelated edits and noisy CI runners β this is the payoff of deoptkit's structural-vs-timing split. On GitHub Actions, new findings also emit ::warning annotations at their source-mapped positions. Commit .deopt/baselines/ to make "no new megamorphic sites" a guarded invariant.
| Signal | What it tells you |
|---|---|
| Megamorphic IC | A property/call site saw 5+ object shapes; V8 fell back to generic hash lookups there |
| Deopt loop | TurboFan repeatedly optimized and discarded the same code β type instability after warm-up |
| Map churn | One constructor/site produces many hidden classes for conceptually one type (conditional or variably-ordered property init) β the root cause behind polymorphic reads elsewhere |
| Profile ticks | Where CPU time actually goes; severity ranking weights every other signal by this |
The project uses Vite+ as a unified toolchain (Oxlint + Oxfmt + tsdown + Vitest) and Bumpy for versioning and release.
vp install # install dependencies
vp check --fix # format + lint + typecheck (with autofixes)
vp test # run Vitest (generates real V8 logs from fixtures/workloads)
node fixtures/generate.mjs # regenerate fixture logs by hand for inspectionTests generate real V8 logs at run time by executing the pathological workloads in fixtures/workloads/ under logging flags β nothing is mocked, so a Node upgrade that changes the log format fails loudly. Design and roadmap: docs/SPEC.md and docs/BENCHMARKING.md.
deoptkit derives its log-parsing approach from deoptexplorer-vscode (MIT), which in turn incorporates code from V8's tick processor (BSD-3-Clause) and thlorenz/deoptigate (MIT). Portions of src/parser/csv.ts are derived from V8's tools/csvparser.mjs. See THIRD_PARTY_NOTICES.md.
Released under the MIT license Β© Drake Costa.