Make the component library tree-shakeable - #402
Open
jasverix wants to merge 1 commit into
Open
Conversation
Importing a single component pulled the whole library into the consumer
bundle: a trivial BccBadge cost 823 kB, and 785 kB arrived before any
component was referenced at all.
Two causes, and fixing either alone changes nothing:
- dist/component-library.js was a single 2.59 MB ES module. Bundlers shake
at module granularity, so with one module every top-level statement whose
purity Rollup cannot prove has to be kept. PrimeVue's per-component style
modules call BaseStyle.extend() and the theme preset calls definePreset()
at module top level; concatenated into one file those become impure
top-level statements that are unreachable but not removable.
- package.json declared no sideEffects, so consumers had to assume the whole
file was side-effectful.
The ES build now emits one file per module (output.preserveModules) and the
package declares "sideEffects": ["**/*.css"]. Dependencies are re-rooted
under dist/vendor/<package>/... because npm strips node_modules directories
from published tarballs and Rollup's default naming would put them there.
Measured against the packed tarball, with a Vue-only baseline of 58 kB:
{ BccBadge } 823 kB -> 62 kB
{ BccBadge, BccButton, BccTag } 851 kB -> 164 kB
{ BccDataTable, BccColumn } 1171 kB -> 554 kB
Cost is now proportional to what is imported: a Badge+Button+Tag bundle
carries only the button, badge and ripple styles, no datatable or galleria.
src/ is unchanged, so the public API is identical. PrimeVue deliberately
stays bundled rather than external: the @primevue/icons pnpm patch that
swaps in @bcc-code/icons-vue only reaches consumers through our own build
output, so externalizing it would silently revert every BCC icon.
preserveModules only supports the ES format, so the UMD bundle moves to its
own config (vite.config.umd.ts). Its output is byte-identical to before,
including the runtime <style> injection that require() consumers rely on.
scripts/check-bundle-size.mjs bundles a probe app against dist and fails if
one component costs more than 40 kB on top of Vue; it runs in CI after the
build. Verified it reports 746 kB and exits non-zero when sideEffects is
removed.
Styles are still not tree-shaken; style.css continues to carry rules for
every component. That needs its own change.
Closes #369
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HgbLt83q8RxauvLrQuXARD
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.
Change summary
Importing a single component pulled the whole library into the consumer bundle. A trivial
BccBadgecost 823 kB, and 785 kB arrived before any component was referenced at all. The ES build now emits one file per module and the package declaressideEffects, so consumers pay only for what they import.Measured against the packed tarball in a fresh consumer app, with a Vue-only baseline of 58 kB:
import '@bcc-code/component-library-vue'{ BccBadge }{ BccBadge, BccButton, BccTag }{ BccDataTable, BccColumn }Cost is now proportional to what you import: a Badge+Button+Tag bundle carries only the button, badge and ripple styles — no datatable, no galleria.
Why it was broken
Two causes, and fixing either one alone changes nothing:
dist/component-library.jswas a single 2.59 MB ES module. Bundlers tree-shake at module granularity, so with one module every top-level statement whose purity Rollup can't prove has to be kept. PrimeVue's per-component style modules callBaseStyle.extend()and the theme preset callsdefinePreset()at module top level; concatenated into one file those become impure top-level statements — unreachable, but not removable.package.jsondeclared nosideEffects, so consumers had to assume the whole file was side-effectful.Sourcemap-level breakdown of the 785 kB floor, plus the full investigation, is in #369.
What changed
vite.config.ts— ES build usesoutput.preserveModules, one file per module. Dependencies are re-rooted underdist/vendor/<package>/..., because npm stripsnode_modulesdirectories from published tarballs and Rollup's default naming would put them underdist/node_modules/.pnpm/<pkg>@<version>_<hash>/....cssCodeSplit: falsekeeps every stylesheet in a singledist/index.css, as the./style.cssexport promises.package.json— adds"sideEffects": ["**/*.css"];module/exportspoint at./dist/index.js.vite.config.umd.ts(new) —preserveModulesonly supports the ES format, so the UMD bundle gets its own config.scripts/check-bundle-size.mjs(new) + CI step — guardrail, described below.README.md— a consumer-facing note on bundle size, and a maintainer-facing note on why the build output must not be flattened.src/is untouched, so the public API is identical — no breaking change. I initially assumedBccPreset,BccComponentLibraryand the entry'simport './style.css'would have to move to subpath exports; they don't. In per-module output the theme preset becomes its own file inside the package, so our ownsideEffectsflag is enough for consumers to drop it.Two things worth a reviewer's attention
PrimeVue deliberately stays bundled rather than external. Externalizing it looks like the obvious fix, but the
@primevue/iconspnpm patch that swaps in@bcc-code/icons-vueonly reaches consumers through our own build output — externalizing would silently revert every BCC icon. I measured both: identical results either way (62 kB / 164 kB), so keeping it bundled costs nothing. There's a comment in the config so this doesn't get "simplified" later.The UMD output is byte-identical to
main.cmpconfirms it, including the runtime<style>injection thatrequire()consumers rely on — Vite inlines the stylesheet for non-ES formats, and dropping that would have silently unstyled them.Guardrail
This regresses invisibly, so
pnpm run test:bundle-sizebundles a probe app against the builtdistand fails if one component costs more than 40 kB on top of Vue (currently 3.3 kB). It runs in CI after the build. Verified it works: withsideEffectsremoved it reports 746 kB and exits non-zero.Not addressed
style.csscontinues to carry rules for every PrimeVue component (~240 kB). Thetheme.css+ Tailwind path already helps; a real fix needs its own issue.quillis adevDependencyyet lands in the published output viaprimevue/editor. Also its own issue.Verification
pnpm lint,pnpm typecheck— cleanpnpm test:unit— 43 tests passingpnpm build— clean;dist/index.cssis byte-for-byte the same size as before (238,480 bytes)storybook build— succeeds (confirmedpreserveModulesdoesn't leak into Storybook's own build)pnpm pack→ installed the tarball in a fresh consumer app: 832 files, nonode_modulespaths,style.cssresolves, both ESM andrequire()entry points loadBccBadge+BccButton+BccTagwithapp.use(BccComponentLibrary)— correct classes anddata-bcc-nameattributes, 150 exports,BccPresetintactByte counts are from this environment (Vite 7.3.5, Rollup 4.59, PrimeVue 4.5.5) and are version-specific — worth a sanity check against a release build before publishing.
Change type
Closes #369
🤖 Generated with Claude Code
https://claude.ai/code/session_01HgbLt83q8RxauvLrQuXARD
Generated by Claude Code