Skip to content

engine subpath resolves full 39-key surface for single-key consumers (value.js in the entry chunk) — subpath/export split or documented post-first-paint import() #6

Description

@mkbabb

Summary

The @mkbabb/keyframes.js/engine subpath barrel resolves its full heavy-tier surface (39 keys) for a consumer that imports a single key (KeyframesAnimation). Because the barrel is a static re-export, a bundler cannot tree-shake the unused surface out of the entry chunk: value.js symbols land in the emitted bundle even though the app never references them. For a single-key consumer this is pure dead weight on first paint.

This was measured end-to-end on a downstream consumer (a Vue 3 SPA that uses only KeyframesAnimation from /engine) during a keyframes.js 4.x → 5.x migration + a follow-up chunk-splitting experiment.

Evidence (measured)

Main entry chunk, before vs. after adopting keyframes.js 5.x /engine:

raw gzip
pre-migration baseline 164.36 kB 53.42 kB
post-migration (5.x /engine) 207.98 kB 70.16 kB
Δ +43.62 kB (+26.5%) +16.74 kB (+31.3%)

(Isolated-prototype delta was +25.3% raw / +30.2% gzip; the composed downstream tree reproduced +31.3% gzip.)

Direct confirmation of the root cause: grepping the emitted vendor chunk for library fingerprints found value.js symbols present (2 occurrences) despite the app only constructing KeyframesAnimation. So the cost is the barrel pulling its whole heavy tier, not anything the consumer imports.

A manualChunks animation-vendor bucket was built and measured as a candidate mitigation. It relabels the bytes (main chunk drops to 22.23 kB gzip; a new animation-vendor chunk is 140.51 kB raw / 48.33 kB gzip) but does not cut what a first-time visitor downloads — total first-load JS is flat (+0.40 kB gzip, +0.42%). Chunk-splitting only chooses which file the 39-key surface lands in, not whether it ships. It's a second-deploy caching benefit, not a first-load size fix.

Ask

Either would close this for single-key consumers:

  1. Per-symbol export / subpath split so @mkbabb/keyframes.js/engine (or a narrower subpath, e.g. /engine/animation) resolves only the requested symbol's transitive surface, letting bundlers drop the rest — the real fix for first-load bytes.
  2. Document the post-first-paint dynamic import() pattern app-side (defer the /engine import behind import() so the heavy tier loads after first paint / on first interaction that needs it) as the sanctioned consumer workaround until (1) lands.

Happy to share the full build/waterfall harness (Playwright + CDP network-throttle, real gzip-encoded responses) and the per-chunk numbers if useful.

Downstream consumer: single-key (KeyframesAnimation) use of /engine; numbers reproduced on a throttled Playwright load waterfall (40 ms RTT / 5 Mbps), real gzip-encoded HTTP responses.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions