Skip to content

Memoize scanPrototypesFor to avoid per-instance prototype walks#30

Merged
bemky merged 1 commit into
1.xfrom
memoize-scan-prototypes
May 5, 2026
Merged

Memoize scanPrototypesFor to avoid per-instance prototype walks#30
bemky merged 1 commit into
1.xfrom
memoize-scan-prototypes

Conversation

@bemky
Copy link
Copy Markdown
Owner

@bemky bemky commented May 1, 2026

Summary

  • scanPrototypesFor(this.constructor, key) runs four times in KompElement's constructor (bindMethods, assignableAttributes, assignableMethods, events) and twice in TableColumn's — every cell, row, and column construction climbs the prototype chain and rebuilds the same arrays.
  • The result depends purely on the (constructor, key) pair, so memoizing it gives every instance after the first a constant-time lookup.
  • Cache is a WeakMap keyed by the constructor → Map keyed by property name → result array. The cache stores references to the per-prototype values (not copies), so plugin in-place mutations like this.events.push('columnResize', 'rowResize') in resizable.js remain visible whether they run before or after first scan.

Why this is safe

All current call sites use the pattern scanPrototypesFor(...).filter(x => x).reverse().forEach(...). filter returns a fresh array, and the subsequent reverse mutates that fresh array — the cached array is never touched. The doc comment notes that callers should treat the returned array as read-only.

Performance impact

Hottest path: building 1000×10 = 10,000 cells previously did 40,000 prototype walks (4 per cell). After this change it's 4 per Cell class total, then O(1) lookups thereafter.

Test plan

  • Render a table/spreadsheet, confirm cells/rows/columns construct identically (attributes, events, methods all wired up).
  • Confirm Spreadsheet.events includes plugin-added entries (columnResize, rowResize, etc.) and that onColumnResize constructor option still binds.
  • Smoke-test docs build (npm run docs).

🤖 Generated with Claude Code

scanPrototypesFor is called four times in KompElement's constructor and twice
in TableColumn's, climbing the prototype chain each time to merge static class
metadata (assignableAttributes, bindMethods, assignableMethods, events). The
result depends only on the (constructor, key) pair, not the instance, so it's
the same answer for every cell, row, and column built from the same class.

Cache results in a WeakMap keyed by the constructor. The cache holds
references to the per-prototype values, so plugin in-place mutations like
`this.events.push(...)` remain visible after a class is first scanned. All
existing call sites use `.filter(...).reverse().forEach(...)`, which copies
before any reordering, so the cached arrays are never mutated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bemky bemky force-pushed the memoize-scan-prototypes branch from d56ebd1 to 041c452 Compare May 1, 2026 16:19
@bemky bemky changed the base branch from main to 1.x May 1, 2026 16:19
@bemky bemky merged commit 471c9a1 into 1.x May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant