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:
- 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.
- 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.
Summary
The
@mkbabb/keyframes.js/enginesubpath 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.jssymbols 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
KeyframesAnimationfrom/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:/engine)(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.jssymbols present (2 occurrences) despite the app only constructingKeyframesAnimation. So the cost is the barrel pulling its whole heavy tier, not anything the consumer imports.A
manualChunksanimation-vendorbucket was built and measured as a candidate mitigation. It relabels the bytes (main chunk drops to 22.23 kB gzip; a newanimation-vendorchunk 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:
@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.import()pattern app-side (defer the/engineimport behindimport()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.