Skip to content

perf: bundle size optimizations (sideEffects, #private mangling, minify tuning) - #99

Open
DaSchTour wants to merge 3 commits into
mainfrom
perf/bundle-optimizations
Open

perf: bundle size optimizations (sideEffects, #private mangling, minify tuning)#99
DaSchTour wants to merge 3 commits into
mainfrom
perf/bundle-optimizations

Conversation

@DaSchTour

Copy link
Copy Markdown
Member

Summary

Three bundle-size optimizations, no public API or behavior changes:

  1. sideEffects field — the package had none, so bundlers couldn't tree-shake any of it. Only src/index.ts and src/wrappers/react.ts have real import-time side effects (customElements.define), so only their dist counterparts are marked side-effectful. Consumers who only import from keksmeister/core, keksmeister/adapters/*, or keksmeister/i18n/* can now be tree-shaken by bundlers that respect this field.
  2. Native #private fields — TypeScript private members compile to plain public properties, so their names survive minification untouched. Converted internal-only members in src/core/*.ts and src/ui/*.ts to native #private fields/methods, which both esbuild (vite's build) and terser can safely rename to single letters since they're lexically private, never string-keyed. ConsentManager#store and #subjectId were kept as regular TS private because consent-manager.test.ts reaches into them via bracket notation (manager['store']); native private fields don't support that.
  3. Terser tuning — added pure_getters=true to 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 with pure_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.tsbuild.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)

File Before After Δ gzip
raw gzip raw gzip
dist/keksmeister.js 38,894 B 10,781 B 36,041 B 10,316 B −465 B (−4.3%)
dist/keksmeister.min.js 33,646 B 9,964 B 30,752 B 9,536 B −428 B (−4.3%)
dist/keksmeister.umd.cjs 33,940 B 10,077 B 31,084 B 9,664 B −413 B (−4.1%)

CI'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 — clean
  • bun run test — 112/112 passing
  • bun run build — clean, sizes recorded above
  • Smoke-tested dist/keksmeister.js and dist/keksmeister.min.js in a happy-dom sandbox: custom elements register, ConsentManager.acceptAll/rejectAll/isAccepted/getChoices work, <keksmeister-banner> renders into its shadow root with the manager wired through the new #private fields.
  • Reproduced the CI bundle-size check locally — passes.

🤖 Generated with Claude Code

DaSchTour and others added 3 commits July 17, 2026 11:29
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>
@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Bundle Report

Changes will decrease total bundle size by 5.71kB (-7.84%) ⬇️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
keksmeister-Keksmeister-esm 36.04kB -2.85kB (-7.34%) ⬇️
keksmeister-Keksmeister-umd 31.08kB -2.86kB (-8.41%) ⬇️

Affected Assets, Files, and Routes:

view changes for bundle: keksmeister-Keksmeister-umd

Assets Changed:

Asset Name Size Change Total Size Change (%)
keksmeister.umd.cjs -2.86kB 31.08kB -8.41%

Files in keksmeister.umd.cjs:

  • ./src/ui/keksmeister-trigger.ts → Total Size: 2.44kB

  • ./src/core/consent-logger.ts → Total Size: 4.89kB

  • ./src/core/cookie-store.ts → Total Size: 1.92kB

  • ./src/core/service-adapter.ts → Total Size: 1.15kB

  • ./src/ui/keksmeister-banner.ts → Total Size: 14.57kB

  • ./src/core/consent-manager.ts → Total Size: 7.4kB

  • ./src/core/script-blocker.ts → Total Size: 3.08kB

view changes for bundle: keksmeister-Keksmeister-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
keksmeister.js -2.85kB 36.04kB -7.34%

Files in keksmeister.js:

  • ./src/core/service-adapter.ts → Total Size: 1.12kB

  • ./src/ui/keksmeister-banner.ts → Total Size: 14.14kB

  • ./src/core/script-blocker.ts → Total Size: 2.99kB

  • ./src/core/cookie-store.ts → Total Size: 1.86kB

  • ./src/ui/keksmeister-trigger.ts → Total Size: 2.37kB

  • ./src/core/consent-logger.ts → Total Size: 4.74kB

  • ./src/core/consent-manager.ts → Total Size: 7.2kB

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

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