perf: bundle size optimizations (sideEffects, #private mangling, minify tuning) - #99
perf: bundle size optimizations (sideEffects, #private mangling, minify tuning)#99DaSchTour wants to merge 3 commits into
Conversation
Bundlers currently can't tree-shake any part of the package since no sideEffects field was declared. Only src/index.ts and src/wrappers/react.ts have real import-time side effects (customElements.define), so those are the only dist files marked side-effectful; everything else (core, adapters, i18n, other wrappers) is now safely tree-shakeable by consumers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TypeScript `private` members compile to plain public properties, so their names survive minification untouched. Switching to native `#private` class fields/methods across the core and ui classes lets both esbuild (vite build) and terser rename them to single letters, since private names are lexically scoped and can never be accessed from outside the class. `store` and `subjectId` on ConsentManager are kept as regular `private` because consent-manager.test.ts reaches into them via bracket-notation (`manager['store']`, `manager['subjectId']`), which native private fields don't support. Verified terser's plain --mangle already renames #-prefixed private names safely; --mangle-props regex=/^#/ was tested and produces an identical bundle size, so it's not added to the build script. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds pure_getters=true to the terser compress options, letting it treat getter access as side-effect-free (safe here, all getters in the codebase are pure reads). Small but measurable win: ~38 bytes raw / ~12 bytes gzip on keksmeister.min.js. Tested and rejected two other candidates because they measured zero benefit on this codebase: - --ecma 2022: byte-identical output (esbuild's ES2022 output needs no further transform at this ecma level). - passes=2: byte-identical output once combined with pure_getters; a single compress pass already reaches the fixed point. - --mangle-props regex=/^#/: byte-identical to plain --mangle, since terser already renames #-prefixed private class members as part of standard name mangling (they're lexical bindings, not string-keyed properties, so no separate "prop mangling" step is needed). Also tried vite's cssMinify: 'lightningcss' for the inlined banner/ trigger CSS (?inline imports). Output was byte-for-byte identical to esbuild's default CSS minifier, so it added a dependency for zero benefit and was reverted (bun remove lightningcss). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundle ReportChanges will decrease total bundle size by 5.71kB (-7.84%) ⬇️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: keksmeister-Keksmeister-umdAssets Changed:
Files in
view changes for bundle: keksmeister-Keksmeister-esmAssets Changed:
Files in
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Summary
Three bundle-size optimizations, no public API or behavior changes:
sideEffectsfield — the package had none, so bundlers couldn't tree-shake any of it. Onlysrc/index.tsandsrc/wrappers/react.tshave real import-time side effects (customElements.define), so only their dist counterparts are marked side-effectful. Consumers who only import fromkeksmeister/core,keksmeister/adapters/*, orkeksmeister/i18n/*can now be tree-shaken by bundlers that respect this field.#privatefields — TypeScriptprivatemembers compile to plain public properties, so their names survive minification untouched. Converted internal-only members insrc/core/*.tsandsrc/ui/*.tsto native#privatefields/methods, which both esbuild (vite's build) and terser can safely rename to single letters since they're lexically private, never string-keyed.ConsentManager#storeand#subjectIdwere kept as regular TSprivatebecauseconsent-manager.test.tsreaches into them via bracket notation (manager['store']); native private fields don't support that.pure_getters=trueto the compress pass (small, safe win — all getters in the codebase are pure reads).Tested and rejected (documented so it isn't retried)
--ecma 2022: byte-identical output.passes=2: byte-identical once combined withpure_getters— one pass already reaches the fixed point.--mangle-props regex=/^#/: byte-identical to plain--mangle. Terser already renames#-prefixed private class members as part of standard name mangling (verified empirically), so no extra flag is needed.vite.config.ts→build.cssMinify: 'lightningcss': byte-for-byte identical output to esbuild's default CSS minifier for the?inline-imported banner/trigger CSS. Added the dependency, measured, then reverted (bun remove lightningcss) since it added a dependency for zero benefit.Size table (raw / gzip)
dist/keksmeister.jsdist/keksmeister.min.jsdist/keksmeister.umd.cjsCI's bundle-size gate (
dist/keksmeister.js, gzip) checks against a 12 kB limit (.github/workflows/ci.yml, not the 10 kB originally assumed) — comfortably passes at 10,316 B (10.07 kB).Test plan
bun run typecheck— cleanbun run test— 112/112 passingbun run build— clean, sizes recorded abovedist/keksmeister.jsanddist/keksmeister.min.jsin a happy-dom sandbox: custom elements register,ConsentManager.acceptAll/rejectAll/isAccepted/getChoiceswork,<keksmeister-banner>renders into its shadow root with the manager wired through the new#privatefields.🤖 Generated with Claude Code