perf(core): lazy-load ConsentLogger - #100
Open
DaSchTour wants to merge 1 commit into
Open
Conversation
ConsentManager statically imported ConsentLogger and paid its ~2.3 kB gzip cost even when `config.logging` was never set, which is the common case. ConsentManager now dynamically imports the implementation only when logging is configured, via a small LazyConsentLogger facade that queues log()/logSnapshot() calls made before the import resolves and replays them in order once it does — no record is ever dropped. The facade is also what's exported as ConsentLogger/createConsentLogger from the package root, since Rollup can't chunk-split a module that's also statically re-exported (confirmed via INEFFECTIVE_DYNAMIC_IMPORT). keksmeister/core (built via tsc, not bundled) keeps exporting the real, eager class unchanged for standalone/headless use. Build: vite.config.ts gives the logger chunk a stable, non-hashed name (chunkFileNames) so the build script can terser-minify it too. Rollup automatically inlines the dynamic import for the UMD output (which can't code-split) while still splitting it for the ES output, so no extra rollupOptions.output config was needed for that split. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundle ReportChanges will increase total bundle size by 1.55kB (2.13%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: keksmeister-Keksmeister-esmAssets Changed:
Files in
view changes for bundle: keksmeister-Keksmeister-umdAssets Changed:
Files in
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ConsentManagerstatically importedConsentLoggerand paid its full cost even whenconfig.loggingwas never set — the common case, since most consumers don't configure server-side logging. This makes the logger implementation load lazily, only when it's actually needed.Design
src/core/lazy-consent-logger.ts(new) —LazyConsentLoggeris a small facade with the same public shape as the realConsentLogger(log,logSnapshot,flush). Its constructor kicks offimport('./consent-logger.js'); calls made before that resolves are queued and replayed in order once it does — no record is ever silently dropped.createConsentLogger()now returns aLazyConsentLogger.src/core/consent-manager.ts— constructs aLazyConsentLoggerinstead of the real class whenconfig.loggingis set.applyChoices()/revokeAll()/sendConfigSnapshot()call sites are unchanged (same.log()/.logSnapshot()shape).src/index.ts(package root) — re-exportsLazyConsentLoggerasConsentLogger, pluscreateConsentLogger, instead of the real eager class.keksmeister/core(built separately viatsc, not bundled — see Build below) keeps exporting the real, eagerConsentLogger/createConsentLoggerunchanged, for standalone/headless use where eager construction is expected.Why the facade is also the public export
I initially planned to leave the root's
ConsentLogger/createConsentLoggerexports pointing at the real class, and only makeConsentManager's internal usage lazy. That doesn't work: Rollup refuses to chunk-split a module that's also reachable via a static import (it logsINEFFECTIVE_DYNAMIC_IMPORTand merges everything into the main chunk — verified with a minimal repro before committing to this design). Sincesrc/index.tsre-exportsConsentLoggeras a real value (sonew ConsentLogger()works standalone), any static reference to the real class from the bundled entry defeats the lazy-loading goal entirely — zero byte savings.The fix: root exports the lazy facade under the name
ConsentLogger. It's fully API-compatible (new ConsentLogger(opts),.log(),.logSnapshot(),.flush()all work identically, with buffering making the timing transparent) — the only observable difference isinstanceofagainst the real class, which isn't part of the documented API. I also had to stop routingsrc/index.ts's other core exports (ConsentManager,CookieStore,ScriptBlocker,ServiceRegistry) through thecore/index.tsbarrel, since that barrel itself still statically re-exports the realConsentLogger(for the/coresubpath) — importing anything from it pulled that static edge back into the bundled entry's graph. They're now imported directly from their own modules instead.Build changes
vite.config.ts: addedbuild.rollupOptions.output.chunkFileNames: '[name].js'so the logger chunk gets a stable name (dist/consent-logger.js) instead of a content hash — needed so the build script below can target it by a fixed path.formats-specific output config was needed for the UMD-vs-ES-splitting constraint: Vite/Rollup already handles it per format automatically — the ES output code-splits the dynamic import intodist/consent-logger.js, while the UMD output (which can't code-split) transparently inlines it, sodist/keksmeister.umd.cjsstays a single self-contained file.package.jsonbuildscript: added a secondterserinvocation that minifiesdist/consent-logger.jsin place (with proper sourcemap chaining), so CDN/keksmeister.min.jsusers get minified logger code too. Bothkeksmeister.jsandkeksmeister.min.jsimport the same./consent-logger.jsspecifier, so they share the one (minified) chunk file.dist/index.jsand the othertsc-built subpath outputs (dist/core/*,dist/adapters/*, ...) are unaffected — confirmed by inspectingdist/core/index.js(still exports the realConsentLoggerunchanged) anddist/index.js(tsc's own emit ofsrc/index.ts, unused by the.export map entry but built correctly regardless)..github/workflows/ci.yml's bundle-size check (gzip(dist/keksmeister.js) <= 12 kB) needed no changes — it already measures only the main chunk, which is now smaller since the logger chunk is separate. New size: 9.69 kB gzip, well under the limit.Size delta
dist/keksmeister.js(ES, main)dist/keksmeister.min.js(CDN, main)dist/keksmeister.umd.cjs(self-contained)dist/consent-logger.js(new, lazy chunk, minified)loggingis configuredNotes:
require/<script>non-module) consumption path; UMD isn'tpackage.json's primary format (module/exports.importpoint at the ES build).Verification
bun run typecheck— clean.bun run test— 118/118 passing, including:src/core/lazy-consent-logger.test.tscovering buffering, in-order replay,createConsentLogger(), andflush().consent-manager.test.tscase asserting a consent decision made immediately after construction (before the dynamic import resolves) is still delivered, viavi.dynamicImportSettled().consent-manager.test.tscases updated toawait vi.dynamicImportSettled()— logging is now asynchronous by nature, and without settling before a test ends, a pending replay leaked into and inflated a later test's mock call count (fixed).bun run build— clean; confirmeddist/consent-logger.jsis only referenced viaimport()indist/keksmeister.js/.min.js, and is absent fromdist/keksmeister.umd.cjs's dependency list (fully inlined there instead).LazyConsentLogger's constructor, whichConsentManageronly invokes whenconfig.loggingis truthy — unchanged from before, just swapped to the lazy class.🤖 Generated with Claude Code