A music-streaming web app uses 60 of its design system's 1,200 icons, yet all of them ship to users. How do you diagnose and fix it?
answer
- what can the build prove unused?
- lookup by name string
- one file that holds everything
- names arriving from the server
- per-row repetition vs shared definition
basics
~20 sSomething references every icon: a name-to-icon map, an icon font or full sprite, or a pre-bundled package. Find it with a bundle report, then use static per-icon imports, an app-specific sprite, or on-demand loading for runtime names.
solid answer
~50 sA build can drop unused icons only when it can **prove** they are unused, which needs static, per-icon references to side-effect-free modules. I would open a bundle report, confirm the icon package's share, and look for what references everything. The usual culprit is an icon component that takes a name string and resolves it through a map holding all 1,200 icons — the build cannot know which strings occur. Others are an icon font or a full sprite, a package published as one pre-bundled file, or a module marked as having side effects. Fixes: import icons individually and statically; for names that arrive from the server, such as genre icons, load each on demand or keep a map of only the allowed names; generate a sprite with only the icons the app uses. Then add a size budget in CI so it cannot regress.
code
pseudocode · 12 lines// keeps all 1,200: the build cannot know which names occur
ALL_ICONS = { play: PlayIcon, shuffle: ShuffleIcon, ...1,198 more }
Icon(name) -> render ALL_ICONS[name]
// keeps only what is referenced
use PlayIcon from icons/play
use ShuffleIcon from icons/shuffle
PlayButton -> render PlayIcon
// data-chosen names: bounded, loaded on demand
GENRE_ICONS = { jazz: load(icons/genre-jazz), rock: load(icons/genre-rock) }
GenreIcon(name) -> render GENRE_ICONS[name] or render DefaultGenreIcongo deeper
Recall that a build can drop unused icons only when each is referenced directly, and that looking icons up by name string usually keeps them all.
Explain why side effects, pre-bundled packages, fonts and full sprites defeat tree-shaking, and how static per-icon references fix it.
Show the diagnosis path with a bundle report, split static from data-driven names, and add a CI budget so the fix does not regress.
Weigh per-icon modules against shared definitions for repeated lists, and decide what the library must publish so every consuming app benefits by default.
## What tree-shaking needs **Tree-shaking** is a build step that removes code no one references. It works on proof, not on guesses: a bundler removes an icon only when it can see, statically, that nothing imports it, and when importing the module has no **side effects** that must run anyway. Anything that references the whole set — even indirectly — keeps the whole set. ## Likely causes, in order of how often they appear | Cause | Why everything ships | Tell-tale sign | |---|---|---| | Icon component takes a name string and looks it up in a map | The map references all icons; the build cannot know which strings occur | One large module named like the icon registry | | Icon font or complete sprite | The file holds every glyph by design | A single font or sprite asset of fixed large size | | Package published as one pre-bundled file | There are no separate modules left to drop | One big file in the package, no per-icon files | | Modules flagged as having side effects | The bundler must keep them in case the side effect matters | Unused icons appear despite static imports | | Names built at runtime from data | Server-supplied names force a full lookup | Genre or mood icons chosen by API values | ## Diagnosing it 1. **Measure.** Produce a bundle report and find the icon package's share. 1,200 icons at even a few hundred bytes each is large compared with 60. 2. **Find the reference that holds everything.** Search the codebase and the design system for a name-to-icon map, a single entry file that re-exports everything with side effects, or a sprite or font asset. 3. **Check how the package is built.** If the design system publishes a single pre-bundled file instead of per-icon modules, the app cannot fix it alone — that is a packaging change in the library. 4. **Separate static from dynamic names.** Most icons in the app are chosen in code (“play”, “shuffle”); a few are chosen by data (genre icons from the catalogue service). They need different fixes. ## Fixing it - **Static names: import each icon directly.** A play button references the play icon module itself, not a string. The bundler can now see exactly which 60 are used. - **Dynamic names: bound the set.** Keep a map of only the icons the data can legitimately name, or load each icon on demand when the name arrives, with a neutral fallback for unknown names. - **Sprites: generate one per app.** A build step collects the icons the app references and writes a sprite with only those. - **Icon font, if still present: subset it** to the used glyphs as an interim step while migrating to vectors. - **Guard it.** Add a bundle-size budget for the icon share in CI so a new name-based lookup fails the build instead of shipping. ## The other cost: repetition in the rendered screen Bytes in the bundle are only half the picture. A track list renders 50 rows, each with like, download and more-actions icons. If every icon is drawn inline, the same path data appears 150 times in the rendered screen, which costs memory and rendering work. A **shared definition** — a sprite symbol referenced by each row — draws the path data once. Many systems combine the two: per-icon modules for tree-shaking, rendered through shared definitions where a list repeats the same icon many times. ## Native mobile has the same shape Native apps package icons as resources. Platforms offer unused-resource removal, but it faces the same limit: a resource looked up by a computed name cannot be proven unused, so such lookups keep everything they might reach. The fix is identical — reference icons statically where possible, and bound the set that data can name. ## Why the library should make the right thing the default Every one of these causes can be introduced by a single well-meaning convenience in the design system itself: a friendly icon component that accepts any name, an entry file that registers every icon on import, a build that concatenates the set into one file. Each consuming app then pays for 1,200 icons whether it uses 60 or 600. The durable fix is therefore on both sides: - the **library** publishes each icon as its own side-effect-free module, and documents static references as the default usage; - a name-based component, if the system offers one at all, is documented as the tool for data-driven names only, with a bounded set; - each **app** keeps a size budget so its own code cannot undo what the library enabled.
- The design system ships one pre-bundled file of all icons. Can the app team fix that alone?Not fully. With one pre-bundled file there are no per-icon modules left for the app's build to drop. The app can copy the 60 icons locally as a stopgap, but the real fix is in the library: publish each icon as its own side-effect-free module so every consuming app gets tree-shaking without workarounds.
- Is inlining each icon always better than a sprite for performance?No. Inlining gives precise tree-shaking and no extra request, but repeats path data every time an icon renders, which hurts long repeated lists. A sprite shares one definition across uses but, unless generated per app, carries unused icons. Many systems use per-icon modules and render repeated icons through shared definitions.
- How do you keep this from regressing after the fix?Add a budget for the icon share of the bundle in CI, so a new lookup-by-name map or an accidental full import fails the build. A lint rule that forbids importing the whole icon package in app code catches the most common reintroduction before the budget even trips.
saying these in an interview costs you the question
- Tree-shaking removes unused icons automatically, however they are referenced.
- Looking icons up by a name string is free for bundle size.
- A sprite always contains only the icons an app uses.
- Inline icons are always faster than shared sprite definitions.
- Native apps never carry unused icon resources.