Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
adobe avatar

Performance

  • 1 installs
  • 12 repo stars
  • Updated July 31, 2026
  • adobe/data

performance is a Claude Code skill that refactors closure-based factories in the @adobe/data library into classes to speed up V8 hot dispatch loops.

About

This skill is a performance-tuning recipe for the @adobe/data TypeScript library. It shows how to refactor closure-returning factories (Column, TypedBuffer, ManagedArray) into classes so V8's inline cache stays monomorphic and methods inline at hot per-row call sites. Developers reach for it when a tight loop dispatches through many object instances and profiling shows the closure shape is the bottleneck. It also sets strict benchmark thresholds so changes are only kept when the measured win is real.

  • Closure-to-class refactor recipe for V8 hot dispatch sites in @adobe/data
  • Ships accept/reject thresholds (>=25% focused bench, >=10% full perftest) to keep or revert
  • Explains the hidden-class / inline-cache reasoning behind the speedup

Performance by the numbers

  • 1 all-time installs (skills.sh)
  • Ranked #982 of 1,352 Code Review & Quality skills by installs in the Skillselion catalog
  • Data as of Jul 31, 2026 (Skillselion catalog sync)
At a glance

performance capabilities & compatibility

Free; no API keys, runs locally against the @adobe/data repo

Capabilities
performance optimization · code refactoring · benchmarking
Use cases
refactoring · debugging
Pricing
Free
From the docs

What performance says it does

…creates a **fresh hidden class plus fresh per-instance methods every call**.
SKILL.md
Keep the change only if focused bench is **≥25% faster** *and* the relevant full-perftest row is **≥10% faster**, with `pnpm test` and `npx tsc -b` clean.
SKILL.md
The interface stays a structural type — `Column<T>`, `TypedBuffer<T>`, `ManagedArray<T>`.
SKILL.md
npx skills add https://github.com/adobe/data --skill performance

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs1
repo stars12
Last updatedJuly 31, 2026
Repositoryadobe/data

What it does

Optimize a hot per-row loop in @adobe/data by converting closure factories to classes when the inline cache goes megamorphic.

Who is it for?

V8 hot loops in @adobe/data where a factory is called many times and a per-row loop dispatches through several instances at one call site.

Skip if: One-off configs, cold setup/teardown paths, or single-instance services where the inline cache is already monomorphic.

When should I use this skill?

Optimizing a hot path in @adobe/data where an interface has many concrete instances created from closure-returning factories.

What you get

Instances share one hidden class and one set of prototype methods so the inline cache monomorphizes and V8 inlines the accessor.

  • Class-based implementation behind an unchanged exported factory
  • before/after benchmark numbers
  • hidden-class sanity check via %HaveSameMap

By the numbers

  • Three boxes must all be ticked before applying
  • Reject unless focused bench is >=25% faster and full-perftest row >=10% faster
  • Benched struct-class readout at N=1M got 10.90 ms -> 10.60 ms (-2.8%)

Files

SKILL.mdMarkdownGitHub ↗

Closure→class refactor for hot dispatch sites

The problem in 30 seconds

A factory shaped like this:

function createColumn(array) {
  return {
    get(i)    { return array[i]; },
    set(i, v) { array[i] = v; },
  };
}

…creates a fresh hidden class plus fresh per-instance methods every call. When a tight loop sees several such instances at the same call site (positionX.get(i), positionY.get(i), velocityX.get(i), …), V8's inline cache goes polymorphic → megamorphic and the methods can't be inlined. Each .get call dereferences the closure context to read array. Same data flow, but the loop is 5× slower than it needs to be.

Replace with a class — class Column { array; constructor(a) { this.array = a; } get(i) { return this.array[i]; } } — and every instance shares one hidden class and one set of prototype methods. The IC monomorphizes, V8 inlines .get, and array becomes a fast property load.

When to look for it

Three boxes must all be ticked:

1. Many instances. The factory is called once per column, once per archetype, once per buffer — not once per app. 2. A hot per-row loop reads or writes through the returned object's methods. ECS systems, render loops, serialization sweeps — anywhere the call count is N × elements. 3. The call site sees more than one such instance. positionX.set(...); positionY.set(...); positionZ.set(...); — three different receivers at the same .set IC.

If even one box is missing, leave the closure shape alone — it's clearer.

When not to apply it

  • One-off configs, plugin descriptors, options bags. The shape doesn't matter; clarity does.
  • Cold paths: setup, teardown, migration, persistence. If it runs once per session, do not classify.
  • Single-instance services where the call site only ever sees one shape — V8's IC is already monomorphic.
  • Anywhere the closure carries non-trivial captured state that would feel awkward as a private field. Force-fitting state onto this for V8's benefit is the wrong trade if it harms readability.

Refactor recipe

Mirror the existing precedents:

  • packages/data/src/cache/managed-array.tsManagedTypedArrayColumn
  • packages/data/src/typed-buffer/create-number-buffer.tsNumberTypedBuffer

Specifically:

1. Methods go on the class body (so they live on the prototype, not on each instance). 2. State goes on this; assign every field in the constructor in the same order across all instances. Use readonly where the field never reassigns. 3. Pull internal helpers up as private methods, not nested closures or arrow-function fields. Arrow-function class fields go on each instance, not the prototype, and reintroduce the slow shape. 4. Don't add or delete properties after construction — that mutates the hidden class. 5. The factory function (createXxx(...)) stays exported and unchanged for callers; only the body is return new XxxClass(...).

Public-API discipline

The interface stays a structural type — Column<T>, TypedBuffer<T>, ManagedArray<T>. Implementations may be classy; consumers must still see what looks like a plain interface object.

  • Don't export the class. Module scope only. Export the factory and the type.
  • Don't add methods that aren't in the interface. A consumer using public types must never need instanceof MyClass.
  • Don't lean on prototype tricks at the boundary. Cloning, persistence, normalization, JSON.stringify — all should work whether the value is a class instance or a plain object.

This protects the data-oriented promise at the boundary. Inside the box: do whatever the profile says. At the box wall: a value is just data.

Verifying the win

In order, three pieces:

1. Focused micro-bench. Use the perftest harness (packages/data/src/perftest/perf-test.ts) — call test.run() directly, with the harness's auto-tuned N and inner-loop sampling. Capture ms/iter before and after. 2. Full perftest. Run packages/data/src/perftest/index.ts in a headless Chromium against the local dist; check the relevant rows row-by-row. Confirm no regressions in unrelated rows. 3. Optional sanity check. Launch Chromium with --js-flags="--allow-natives-syntax" and console.log(%HaveSameMap(positionX, velocityX)) — should be true after the refactor, false before.

Reject thresholds. Keep the change only if focused bench is ≥25% faster and the relevant full-perftest row is ≥10% faster, with pnpm test and npx tsc -b clean. Anything less is noise; revert.

Things that are not this optimization

Don't conflate the closure→class fix with adjacent perf problems — they have different cures.

  • Per-element allocation in hot reads (e.g. struct buffer's get(i) returning a fresh {x,y,z}). The cure is bulk APIs (forEach, getInto(target, i)) or operating on the underlying typed array directly, not class-ifying the readout. Empirically confirmed: we benched a generated class for struct readout in this repo (`{x: f32, y: f32, z: f32}`, N=1M) and got 10.90 ms → 10.60 ms (−2.8%) — well below the 25% threshold. V8 already converges on a stable hidden class for object literals whose properties are assigned in fixed order, so the class buys nothing here. Don't re-try this without first changing the surface (e.g. a `getInto(target, i)` that mutates a caller-owned object, eliminating the allocation entirely).
  • Polymorphic IC caused by mixed value types (numbers and strings flowing through the same column). Cure: type-specialized columns.
  • Closure-captured bindings that get reassigned (array = allocator.refresh(array) etc.). The hidden class is wrong from instance #1 — class form is correct independent of instance count.

In-repo precedents

  • packages/data/src/cache/managed-array.tsManagedTypedArrayColumn. Original case study; 5.4× on the focused profile, 30% on ec2s:move column.
  • packages/data/src/typed-buffer/create-number-buffer.tsNumberTypedBuffer. The TypedBuffer family (number, struct, const, enum, array) is uniformly class-based; that's the standard to follow.
  • packages/data/src/perftest/perf-test.ts — harness with warmup + auto-tuned N + inner-loop sampling. Use it for any new benchmark.

Related skills

FAQ

When should I apply the closure-to-class refactor?

Only when three boxes are ticked: many instances, a hot per-row loop reads or writes through the returned object's methods, and the call site sees more than one such instance.

How do I know the refactor was worth keeping?

Keep it only if the focused micro-bench is at least 25% faster and the relevant full-perftest row is at least 10% faster, with pnpm test and npx tsc -b clean; otherwise revert.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.