Skip to content

perf: shrink bundle via listener helpers and shorter internal names#146

Draft
layershifter wants to merge 9 commits intomicrosoft:mainfrom
layershifter:perf/shrink-internals
Draft

perf: shrink bundle via listener helpers and shorter internal names#146
layershifter wants to merge 9 commits intomicrosoft:mainfrom
layershifter:perf/shrink-internals

Conversation

@layershifter
Copy link
Copy Markdown
Member

Summary

Targeted follow-up to the bundle-size stack. A token-frequency analysis of the minified output on top of #145 identified addEventListener, removeEventListener, lastFocusedProgrammatically, and __keyborgNativeFocus as the four costliest unmangled tokens (together ~1.2 kB of the 5 kB minified bundle). This PR attacks each.

  • New src/dom.mts exporting on(target, type, handler) / off(target, type, handler) — thin wrappers over the capture-phase DOM pattern that every keyborg listener uses. 44 call sites (22 in FocusEvent.mts, 12 in Keyborg.mts) now route through them, so the minifier only sees the long DOM method names twice.
  • Internal field renames on __keyborgData: focusInHandler → _fi, focusOutHandler → _fo, lastFocusedProgrammatically → _lfp, shadowTargets → _st. Nothing outside FocusEvent.mts reads these.
  • __keyborgNativeFocus → _n. Stored on the overridden focus function to retain the native implementation. Only nativeFocus() / setupFocusEvent() / disposeFocusEvent() reference it — all internal.
  • Hoisted doc / proto locals in setupFocusEvent() so repeated kwin.document / kwin.HTMLElement.prototype accesses mangle.

The outer __keyborgData key name is preserved (any external consumer — e.g. Tabster — probing the window is untouched).

Stacking

Stacked on #145 (perf: raise build target to ES2022). Until #145 merges, this PR's diff will include #145's commit. Please review only the final commit (012219a); earlier commits are already under review in #143 / #144 / #145.

Bundle size

Baseline = post-#145 tip.

Fixture Before (min / gz) After (min / gz) Delta
All exports 4.632 kB / 1.655 kB 3.892 kB / 1.624 kB −740 B min (−16%) / −31 B gz (−1.9%)
createKeyborg() & disposeKeyborg() 4.435 kB / 1.615 kB 3.754 kB / 1.584 kB −681 B min (−15%) / −31 B gz (−1.9%)
KEYBORG_FOCUSIN constant 64 B / 80 B 64 B / 80 B 0

Gz delta is modest because repeated tokens compress well anyway; the real win is in the raw minified size, which matters for HTTP/2 and for consumers with gzip disabled.

Cumulative vs main (entire stack)

Fixture main min / gz This branch min / gz Total delta
All exports 6.991 / 2.077 kB 3.892 / 1.624 kB −3.099 kB (−44%) / −453 B (−22%)
createKeyborg+disposeKeyborg 6.755 / 2.028 kB 3.754 / 1.584 kB −3.001 kB (−44%) / −444 B (−22%)

dist/index.js (raw bundle before minification): 16.36 kB → 10.45 kB (−36%).

Notes for reviewers

  • Source is slightly more ceremony-heavy than before (helper calls, shorter names) but each change is narrow and has a tangible byte cost attached.
  • If preserving __keyborgNativeFocus as a named marker for devtools inspection is important, we can use a longer but still mangle-friendly name (e.g. _nativeFocus) — tell me and I'll adjust.
  • The event.details (plural) legacy field in KeyborgFocusInEvent is still present; dropping it would be another ~30 B but is a breaking API change, so held off.

Test plan

  • npx eslint src/ tests/ clean
  • npm run format clean
  • npm run build succeeds (dist/index.js 12.42 kB → 10.45 kB)
  • npm test — 9/9 Playwright specs pass locally
  • npm run bundle-size shows shrinkage on both non-trivial fixtures

🤖 Generated with Claude Code

layershifter and others added 9 commits April 22, 2026 19:49
Four-PR plan to shrink keyborg's emitted bundle: drop the IE11
WeakRef shim, refactor classes to closures for better minification,
then measure ES2022 target and plain tsc emit as spikes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First implementation plan in the bundle-size reduction stack.
Subsequent PRs (closure refactor, ES2022 spike, tsc-emit spike)
will get their own plans as the stack progresses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
.browserslistrc excludes IE 11, so the shim's fallback path was
unreachable. Replace with native WeakRef, remove the companion
Disposable interface, and delete the unused KeyborgCore.isDisposed()
method (its body `return !!this._win` was inverted — returned true
before dispose — which confirms nothing on the supported path ever
called it).

Bundle-size deltas on measurement fixtures:
- All exports:                        -385 B min / -120 B gz
- createKeyborg + disposeKeyborg:     -385 B min / -124 B gz
- KEYBORG_FOCUSIN constant:           no change

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The deletion was intended in the previous refactor commit but got
dropped during a stash/pop round-trip. The file is already unused
(no importers) so the bundle shape is unchanged — this just removes
the now-dead file from the source tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces class KeyborgCore and class Keyborg with factory functions.
Per-instance state moves from this._x fields (which minifiers cannot
mangle at target: "es2019") to closure locals (which they can). Public
API unchanged: createKeyborg, disposeKeyborg, and the Keyborg type
(now an interface) keep the same shape. WindowWithKeyborg.__keyborg
retains its outer { core, refs } shape.

Bundle-size deltas vs post-PR-1 baseline:
- All exports:                        -1.693 kB min / -212 B gz
- createKeyborg + disposeKeyborg:     -1.693 kB min / -211 B gz
- KEYBORG_FOCUSIN constant:           no change

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
At es2022, class fields, #private syntax, optional chaining and
nullish coalescing are native — no transpilation helpers. The closure
refactor in microsoft#144 already eliminated the class bodies, so the remaining
wins come from lib-level features.

Bundle-size deltas vs post-microsoft#144 baseline:
- All exports:                       -281 B min / -90 B gz
- createKeyborg + disposeKeyborg:    -242 B min / -78 B gz
- KEYBORG_FOCUSIN constant:          no change

Decision rule (>=2% min shrink, no gz regression) satisfied:
min shrinks 5.2-5.7%, gz shrinks 4.6-5.2%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Targeted follow-up to the bundle-size stack, addressing the four
hottest unmangled tokens identified by token-frequency analysis of the
minified output (addEventListener, removeEventListener,
lastFocusedProgrammatically, __keyborgNativeFocus).

- New src/dom.mts exports on() / off() helpers that wrap the capture-
  phase addEventListener/removeEventListener pattern. 44 call sites
  (22 in FocusEvent.mts, 12 in Keyborg.mts) now route through them, so
  the minifier only sees the long DOM method names twice (in the
  helper bodies) instead of 44 times.
- Internal __keyborgData fields renamed: focusInHandler -> _fi,
  focusOutHandler -> _fo, lastFocusedProgrammatically -> _lfp,
  shadowTargets -> _st. Nothing outside FocusEvent.mts reads these.
- The __keyborgNativeFocus flag (stored on the overridden focus
  function to retain the native implementation) renamed to _n. Only
  nativeFocus() / setupFocusEvent() / disposeFocusEvent() reference it.
- Hoisted kwin.document / kwin.HTMLElement.prototype to local bindings
  (`doc`, `proto`) in setupFocusEvent() to let the minifier mangle
  repeated accesses.

Bundle-size deltas vs post-microsoft#145 baseline:
- All exports:                     -740 B min / -31 B gz
- createKeyborg + disposeKeyborg:  -681 B min / -31 B gz
- KEYBORG_FOCUSIN constant:        no change

Cumulative vs main (entire stack):
- All exports:                     -3.099 kB min (-44%) / -453 B gz (-22%)
- createKeyborg + disposeKeyborg:  -3.001 kB min (-44%) / -444 B gz (-22%)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown

📊 Bundle size report

Package & Exports Baseline (minified/GZIP) PR Change
keyborg
All exports
6.991 kB
2.077 kB
3.892 kB
1.624 kB
-3.099 kB
-453 B
keyborg
createKeyborg() & disposeKeyborg()
6.755 kB
2.028 kB
3.754 kB
1.584 kB
-3.001 kB
-444 B
Unchanged fixtures
Package & Exports Size (minified/GZIP)
keyborg
KEYBORG_FOCUSIN constant
64 B
80 B
🤖 This report was generated against f85618e6335ccb113ed0d126ab3c27a1c4671821

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