Every Phosphor Icon You Import Ships Six Times
Sep 15, 2026

Every Phosphor Icon You Import Ships Six Times

828 Words|5 Minutes to read

I was hunting for the last few kilobytes on my landing page and found 51KB of SVG paths. For 26 icons. That's about 2KB per icon, and most of them are a circle with two lines in it.

The reason is the API. Phosphor icons take a weight prop:

<HeartIcon weight='fill' />

That's decided at runtime. So the component has to carry every weight it might be asked for. Open the package and there it is, a Map with six entries:

const a = new Map([
['bold', ...],
['duotone', ...],
['fill', ...],
['light', ...],
['regular', ...],
['thin', ...],
]);

3,428 bytes raw, 1,186 gzipped. The component that wraps it is 290 bytes.

So importing one icon gives you six. And your bundler can't help. Tree shaking drops unused exports. These are properties of an object that is being used. Nothing here is unreachable code.

This is not you misconfiguring it

Worth saying clearly, because every thread about Phosphor bundle size is answering a different question.

Per-icon shaking works fine. Import { HeartIcon } and you get Heart, not the other nine thousand. The maintainers have closed a few issues confirming this and they're right.

Per-weight is the open one. Issue #148, "Treeshake unused weights", opened September 2025. Still open. Zero replies.

I went looking for an escape hatch anyway. No babel plugin. No @phosphor-icons/react/Heart/bold import path. I read the package's exports map — the only subpaths are per icon. So the 5x is just what the weight prop costs.

I counted what I actually use

Before fixing anything I counted. Whole site:

  • 45 icons
  • 66 weights, so 1.5 per icon on average
  • exactly one icon with a computed weight: the card heart, weight={liked ? 'fill' : 'regular'}

45 icons at six weights each is 270 variants shipped to do the work of 66.

I assumed the fill/regular pairing would be all over the place and would block any fix. It shows up once. (that was the whole reason I hadn't tried this earlier)

Generating just the weights I use

The fix is a build script. It renders Phosphor's own components and keeps the markup. No new dependency, nothing redrawn — it's their SVG, minus the weights I never ask for:

import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import * as Phosphor from '@phosphor-icons/react/dist/ssr';
const html = renderToStaticMarkup(
createElement(Phosphor.HeartIcon, { weight: 'fill' })
);

Strip the outer <svg>, turn the attributes into JSX props, write one file. A manifest says which weights each icon needs. regular is always in, because that's what a bare <HeartIcon /> renders. Anything else you ask for:

const WEIGHTS: Record<string, Weight[]> = {
ArrowsClockwise: ['bold'],
Heart: ['fill'],
HouseSimple: ['fill'],
UsersThree: [],
};

The generated file copies how Phosphor does it. Weights object outside the component so it's built once. Shared base with their exact defaults (viewBox="0 0 256 256", fill="currentColor", size="1em"), so nothing moves on screen.

Whole site now: 45 icons, 66 weights, 33KB raw, 9.5KB gzipped.

I got the number wrong first

I estimated the saving by gzipping the source files and multiplying. 26 icons, 1.2KB each, so roughly 27KB off the landing page. Then I built it and measured every script the page loads:

beforeafter
Landing page JS963KB raw / 302KB gz929KB raw / 294KB gz
SVG paths in those chunks173 (51KB)61 (21KB)

About 7KB gzipped. Not 27.

Gzipping a file on its own tells you very little about what it costs inside a bundle, where the same path syntax is already repeating across every other icon and compressing against it. The honest number here is the path count: 173 shapes down to 61.

I'm writing that down because my estimate was off by 4x, in the direction that would have talked me into spending a week on it.

Is it worth doing

Keep @phosphor-icons/react as your default. Per-icon shaking is fine and 3.4KB only hurts once you have forty of them.

Do the subset when icons are a real share of your bundle and you can list the weights you use. The other option is unplugin-icons with the @iconify-json/ph set, which gives you true per-weight shaking. Catch is each weight is a separate icon name there, so a dynamic weight prop stops working.

Mine lives in scripts/build-icons.ts now. Adding an icon means adding a line and re-running it. That's the tax. Until #148 gets an answer, you pay it one way or the other.

Comments