DEV Community

pooja verma
pooja verma

Posted on

Our icon imports were quietly adding 400KB to every route. Here's how we fixed it

We had a Lighthouse score problem that made no sense. Our marketing pages were lean, but every dashboard route was shipping way more JS than it should've — and none of it showed up as "our code" in the bundle analyzer. It took an embarrassingly long time to realize the culprit wasn't a library or a dependency tree gone wrong. It was icons.

Specifically: we were importing an entire icon package (import { Settings, Bell, User, ... } from 'icon-library') and even with tree-shaking supposedly enabled, our bundler was pulling in far more than we used. Some of it was bad imports on our end. Some of it was the package itself not being structured for tree-shaking at all — no per-icon entry points, just one giant barrel file.

The fix ended up being less "better bundler config" and more "stop depending on a JS icon package at all." SVGs, inlined directly as components, don't have this problem — there's no runtime package to accidentally over-import from.

I pulled our full icon set from scratch off Free SVG Icons instead, since we needed something we could inline as raw markup rather than something wrapped in someone else's component API. A few things made this actually workable instead of just theoretically nice:

  1. Filtering by category kept the set small on purpose.
    Instead of installing a 2,000-icon package and importing 40 of them, we browsed the UI interface icons section directly, grabbed only what the product actually used, and stored the raw SVG markup ourselves. No package, no version to bump, no transitive dependency.

  2. The SVG code editor let us normalize everything before it hit the repo.
    Icons from different sources tend to have inconsistent viewBox values, stray fill attributes hardcoded instead of inheriting currentColor, and junk

    /<desc> tags nobody asked for. We ran each one through the SVG code editor to strip that out and standardize the markup before committing it — something like:</p></li> </ol> <p>jsx<br> // before: fill="#000000" baked in, breaks dark mode<br> <path fill="#000000" d="..." /></p> <p>// after: inherits color, works with our theme system<br> <path fill="currentColor" d="..." /></p> <p>That one change — swapping hardcoded fills for currentColor — fixed a dark-mode bug we'd been ignoring for weeks, as a side effect of just cleaning the SVGs properly.</p> <ol> <li>We built a tiny local icon component instead of a package.</li> </ol> <p>jsx<br> const icons = {<br> bell: BellSvg,<br> settings: SettingsSvg,<br> };</p> <p>export function Icon({ name, ...props }) {<br> const Component = icons[name];<br> return <Component {...props} />;<br> }</p> <p>Nothing fancy. But now every icon we ship is one we explicitly chose, explicitly cleaned, and explicitly own — no barrel file, no surprise 400KB.</p> <ol> <li><p>For anything we needed at multiple sizes or export formats, the image converter handled generating PNG fallbacks for the handful of places (email templates, mostly) where inline SVG wasn't an option.</p></li> <li><p>For teammates working in Figma, the Figma plugin meant design could pull from the same source we were coding against, so we stopped getting handoffs referencing icons that didn't exist in our set.</p></li> </ol> <p>End result: dashboard route bundle dropped by roughly the size of the icon package we removed, dark mode icon bug gone as a freebie, and — maybe the more useful long-term change — icons stopped being a dependency we updated and started being assets we owned.</p> <p>If you're still importing an icon package wholesale and trusting tree-shaking to save you, it's worth actually checking your bundle analyzer. Ours lied to us for months.</p>

Top comments (0)