In a component library, what must the package do so a consuming app's bundler can drop unused components, and what goes wrong with a wrong side-effect declaration?
answer
- statically analysable imports and exports
- one module per component
- work done merely on import
- style files imported for effect
- prove it with a fixture build
basics
~20 sShip a static import-and-export format with one module per component, keep modules free of import-time work, and declare which files have side effects. Declaring everything side-effect free drops imported style files; declaring nothing keeps every component.
solid answer
~50 s**Tree-shaking** is the bundler removing code nothing references, and it can only do that when it can prove the removal is safe. The library helps by shipping a **static import-and-export module format**, keeping **one module per component** instead of one pre-concatenated file, and avoiding **side effects** - work done merely because a module was imported, such as injecting global styles or registering elements globally. It then states in its package manifest which files do have side effects. Get that declaration wrong in one direction - 'nothing has side effects' - and the bundler drops style files imported only for their effect, so components ship unstyled. Get it wrong in the other - no declaration - and the bundler must keep every module the index re-exports, so a telecom app using two components ships all eighty.
go deeper
Recall that bundlers remove unused components only when the library makes that provably safe, and that importing styles counts as a side effect.
Explain static module formats, one module per component, and both failure directions of the side-effect declaration.
Show how you would catch tree-shaking regressions before release with a fixture build, a size budget and a rendered check for missing styles.
Discuss making tree-shakeability a published guarantee of the library and what package-shape rules the team must hold to keep it true.
## What tree-shaking needs **Tree-shaking** (dead-code elimination at module level) is how a bundler removes code a consuming app never uses. For a **component library**, that means an app importing a plan card and a data-usage meter should not ship the other seventy-eight components. The bundler can only drop a module if two things are true: 1. **Nothing references its exports** - which it can only tell if imports and exports are **static**, written in a format the bundler can analyse without running code. 2. **Importing it does no necessary work** - it has no **side effects**. A legacy, runtime-loaded module format fails the first test: which exports are used is decided while the program runs, so the bundler must keep everything. ## What counts as a side effect - Injecting a global stylesheet when the module loads. - Registering a component in a global element registry. - Installing a polyfill or patching a shared global object. - Starting timers or listeners at import time. Defining functions, components and constants is *not* a side effect; nothing happens until something calls them. ## The side-effect declaration Because bundlers cannot always prove a module is effect-free, a package can **declare** it in its manifest - either 'this whole package is side-effect free' or 'only these files have side effects'. The declaration is a promise the bundler trusts: | Declaration | Effect on the consuming app | Risk | |---|---|---| | None | Bundler keeps any module it cannot prove unused | Every re-exported component ships | | Whole package side-effect free | Bundler drops any module whose exports are unused | Style files imported only for effect vanish | | Only style and registration files listed | Bundler drops unused components but keeps listed files | Must be maintained as files are added | The third row is the usual target. The classic production bug is the second: a component module imports its stylesheet purely for the effect, nothing references a value from that import, the bundler trusts the 'side-effect free' promise and removes it, and components render unstyled in production builds only. ## Package shape that helps - **Preserve module structure**: one output module per component, not one concatenated file. A bundler often cannot split a single file cleanly, because its top-level code runs as one unit. - **Per-component entry points** so consumers can import a component directly, while an index that re-exports everything still works when the declaration is right. - **Keep registration explicit**: if components must register globally, expose a function the app calls, rather than registering on import. ## A worked example A telecom self-service app imports a plan card and a data-usage meter through the library's index, which re-exports all eighty components: 1. **Correct declaration** (only style and registration files listed): the output holds the two components, the shared internals they use, and their two style files. 2. **No declaration**: the bundler cannot prove the other seventy-eight modules harmless, so all eighty ship, with every style file. 3. **Over-broad declaration** (everything side-effect free): the two components ship, but their style files - imported only for effect - are removed, and both render unstyled in production. Only the first outcome is what the consuming team expected, and only a build of a real consumer reveals which one the package produces. ## Proving it works Build a tiny fixture app that imports one component and measure its output. Fail CI if the output grows past a budget or contains a unique string from an unrelated component. This catches both failure directions: a missing declaration shows as growth, and an over-broad one as missing styles in a rendered fixture. ## The native mobile parallel Native toolchains strip unused code at link time, and the same principle applies: code reachable only through dynamic lookup or registered at load time is kept, so libraries that want to stay small avoid global registration and dynamic lookups by name.
- Why does a single pre-concatenated file of all components hurt tree-shaking?When every component is concatenated into one module, its top-level code runs as a unit on import, and the bundler must prove each piece both unused and effect-free to drop it - often it cannot. One module per component, with static imports and exports, gives the bundler clean seams to cut along.
- How do you verify a library tree-shakes before publishing?Build a fixture app that imports one component, measure the output against a budget, and search it for a unique string from an unrelated component. Fail CI on either. Render the fixture too, so an over-broad side-effect declaration shows up as missing styles rather than surfacing in a consumer's production build.
- Which code in a library counts as a side effect?Anything that does work merely because the module was imported: injecting global styles, registering elements in a global registry, installing polyfills, mutating shared objects, or starting timers and listeners. Pure definitions - functions, components, constants - are not side effects until something calls them.
Like telling movers every unlabelled box is junk so they can travel light: it works until one of those boxes held the house keys - the style file imported only for its effect.
saying these in an interview costs you the question
- Tree-shaking works automatically whatever module format the library ships.
- Declaring the whole package side-effect free is always safe.
- Importing a stylesheet inside a component module is never a side effect.
- One pre-concatenated file is best because it is a single request.
- Unused components cost nothing because the app never renders them.