npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

deoptkit

v0.3.0

Published

A V8 deoptimization toolkit for AI agents and dev workflows: MCP server, findings engine, benchmark harness, and fix-verify comparison for inline caches, deopts, and hidden-class churn

Readme

🔥 deoptkit

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.

What's in the kit

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() / interleaved() — plant in-log window markers, warm up hot code, and time variants against each other. | | deoptkit/vitest + deoptkit/bench | A vitest bench preset that records V8 logs per worker, a provider that runs each case a fixed number of times, 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. |

Quick start (AI agent + MCP)

For Claude Code the fastest path is the plugin: run /plugin marketplace add Saeris/deoptkit, then /plugin install deoptkit@deoptkit. It registers the server and adds a skill with the workflow. The manual registration below works for any MCP host.

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:

  1. profile_run { command: ["node", "bench.js"] } — runs the workload under V8 logging flags (no flag knowledge needed) and loads the result as a session.
  2. get_findings { sessionId } — everything wrong, ranked worst-first with severity 1–100, source locations, V8-level explanations, and suggested-fix categories.
  3. Fix the top finding — at its original position when present (that's the source you wrote), otherwise its file/line (paths are relative to the project, ready for your editor or file tools).
  4. profile_run again, then compare_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, get_profile (the call tree: who calls the hot function, top-down or bottom-up), 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. The server flags cold runs: profile_run and load_log return quality.warnings when no user function reached an optimizing tier, no profiler ticks were recorded, or the ticks are too sparse to rank by (sparse-ticks: IC and map logging starved the sampler; profile once more with categories: ["profile"] for the call tree). get_findings also lists the files on the page that have no source map under unmapped, so a bundle built without one is not mistaken for the source.

Benchmarking your own code

Standalone scripts

// 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".

To check that a fix is also faster, time the two versions against each other in one process with interleaved(): every round runs each variant once in rotating order, and the medians over rounds are what to compare (back-to-back before/after runs on a busy machine drift by 30% or more).

import { interleaved } from "deoptkit/harness";
import { parseRecord as before } from "../dist-before/index.mjs";
import { parseRecord as after } from "../dist/index.mjs";

const { variants } = interleaved(
  "parse record",
  {
    before: (i) => before(inputs[i % inputs.length]),
    after: (i) => after(inputs[i % inputs.length])
  },
  { rounds: 9, iterations: 20_000 }
);
console.log(variants.before.medianMs, variants.after.medianMs);

Schema-driven inputs (Valibot + Valimock) are the easiest way to get realistically varied shapes — see docs/recipes.md.

Vitest bench mode

Requires Vitest 5, where bench is a test-context fixture.

// 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 { test } from "vitest";
import { benchObserved } from "deoptkit/bench";
import { parseRecord } from "../parse";

test("parse record", async ({ bench }) => {
  await benchObserved(bench, "parse record", () => parseRecord(next())).run();
});

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.) To observe the shipped code at exact source positions, without Vite's module runner in between, use deoptKit({ nativeEsm: true }); see the recipe for its requirements.

Bound the logs with a fixed count. Vitest runs each case for a time budget (one second by default), so under V8 logging a fast case runs hundreds of thousands of times, and with every log category on, one run can write gigabytes. iterations runs each case a fixed number of times instead, with no time budget and no warm-up. deoptkit's benchmark provider applies the count, and you select it in your own benchmark config:

export default defineConfig({
  test: {
    ...deoptKit({ outDir: ".deopt", iterations: 2_000 }),
    benchmark: { provider: "deoptkit/vitest/provider" }
  }
});

A bench that asks for fewer calls keeps its count (.run({ iterations: 5 }) for a slow case stays 5). The count also makes the findings repeat from run to run, which is what deoptkit ci --manifest needs. If iterations is set without the provider, the run stops at once with a message, since every case would otherwise still run for the time budget.

Full recipes, including SSR apps (Next/Astro via deoptkit/serve) and builds, live in docs/recipes.md.

Inline editor diagnostics (LSP)

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.json

The server watches the file and republishes on every change, so vitest bench --watch rewrites .deopt/findings.json on every run and the 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. Findings the committed baseline already accepts (baselined in the interchange, set by deoptkit ci) and findings in code the profiler never sampled (heat of 0) publish as hints, which editors keep out of the Problems panel, so the panel lists only what is new and actually ran.

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 lsp and attach it to javascript/typescript.

VSCode: install the deoptkit extension (marketplace id saeris.deoptkit-vscode). It starts deoptkit lsp for the first workspace folder and shows findings as squiggles and Problems entries. It runs the workspace's installed deoptkit (node_modules/deoptkit/dist/main.mjs, the same version that wrote findings.json) and falls back to the server bundled with the extension when there is none. Three settings: deoptkit.findingsPath (default .deopt/findings.json, relative to the workspace folder), deoptkit.serverCommand (a command such as ["npx", "deoptkit", "lsp"] that replaces both server choices) and deoptkit.enable (default true), which the deoptkit: Toggle Enable/Disable command flips for the workspace when the squiggles are in the way. VSCode + Claude Code users also 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.

CI regression gating

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 baseline

Baselines 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.

Identities use paths relative to the directory deoptkit ci runs in, so run it from the same project directory locally and in CI and the committed baseline matches everywhere.

Findings that sit on a threshold can flicker between runs; build the baseline with deoptkit ci --accumulate over a few runs so that only genuinely new sites fail the gate.

To gate Vitest benches instead of scripts, run them under the preset with a fixed iterations count, then gate the manifest the run wrote:

vitest bench --run                                   # writes .deopt/manifest.json
deoptkit ci --manifest .deopt/manifest.json          # all of the run's logs, one baseline

The manifest lists only the logs that run wrote, and every one must analyze, or the gate exits 2. A gate over part of a run would pass for the wrong reason. Run the same bench files each time: a run over fewer files reports the rest as findings that no longer occur.

This repository gates itself the same way: bench/parse.bench.mjs parses a fixture log under observation and .github/workflows/ci.yml runs deoptkit ci over it against the committed baseline.

What the signals mean

| 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 | | Dictionary mode | Objects at a store site left fast-properties mode (keyed stores past V8's 12-field soft limit, or delete); every access on them is a hash lookup | | Profile ticks | Where CPU time actually goes; severity ranking weights every other signal by this |

IC findings also say what reached the site: receiver: "string" means polymorphism over V8's own string representations (not fixable by property order), "array" means arrays differing only by elements kind (keep each array's element type uniform), and both rank below object-shape polymorphism.

Contributing

The project uses Vite+ as a unified toolchain (Oxlint + Oxfmt + tsdown + Vitest) and Bumpy for versioning and release. Run vp install, vp check --fix, and vp test from the repository root, and vp pack and node fixtures/generate.mjs from this package directory. See the repository README for the repository layout, and docs/SPEC.md and docs/BENCHMARKING.md for the design and roadmap.

Acknowledgements

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.

License

Released under the MIT license © Drake Costa.