DEV Community

Daniel Pertu
Daniel Pertu

Posted on

581 inline icons, 21 shapes between them, and 164 KB of our HTML is the attributes around the drawings

PubTrivia uses lucide-react, which means every icon on the site is an inline <svg> in the server-rendered HTML rather than a file, a font or a sprite. I pulled all 72 public pages and counted them, because "inline SVG is cheap" is one of those things everybody repeats and nobody measures.

581 icons. Here is the whole vocabulary:

Icon Count
arrow-right 219
chevron-right 127
check 90
chevron-down 69
minus 29
x 27
the other 15 20

21 distinct names, and the top six are 561 of the 581. The long tail is one trophy, one wifi, one timer, one smartphone and so on, each appearing once or twice on a single page, which is exactly what a marketing site looks like: a handful of structural arrows doing nearly all the work, and a few illustrative icons on the features index.

Those 581 elements contain 871 <path> children, drawn from 49 distinct d strings. arrow-right is two of them (M5 12h14 for the shaft, m12 5 7 7-7 7 for the head), which is why the single most repeated instruction in our HTML is a horizontal line 14 units long, 249 times.

176 bytes of ceremony, 47 bytes of drawing

Every one of the 581 opening tags carries the same nine attributes:

<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
     fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
     stroke-linejoin="round" class="lucide lucide-arrow-right ..." aria-hidden="true">
  <path d="M5 12h14"></path>
  <path d="m12 5 7 7-7 7"></path>
</svg>
Enter fullscreen mode Exit fullscreen mode

Those nine fixed attributes are 176 bytes. Multiply by 581 and the site ships 99.9 KB of identical SVG boilerplate, of which the xmlns declaration alone, which inline SVG in an HTML document does not need, is 19.9 KB.

Split every icon into its opening tag and its contents across all 72 pages:

Total Mean per icon
Opening <svg> tag 163.9 KB 288 bytes
Everything inside it 26.7 KB 47 bytes

The wrapper is 6.1 times the size of the thing it wraps. The gap between 176 and 288 is the class attribute, which is where Tailwind's sizing and colour utilities land, and where the one genuinely per-instance information lives.

In total, inline icons are 194.0 KB of our 4,962 KB of HTML. 3.9 per cent of every byte this site serves is pictures of arrows written out in full.

And then brotli happens

That number is the one you would put in a ticket. It is also the one that falls apart the moment you compress anything.

I took all 72 pages, stripped every <svg>...</svg> out, and compressed both versions:

Raw brotli -q 11
As served 5,081,288 638,202
With every icon removed 4,882,587 625,142
Cost of 581 icons 194.0 KB 12.8 KB

194 KB of raw text costs 12.8 KB on the wire, about 180 bytes per page. Repetition is precisely what a dictionary compressor is for, and 581 copies of the same nine attributes is the friendliest input it will ever see. The second copy of stroke-linejoin="round" costs a backreference.

Two caveats, because this is the kind of number people quote back at you. First, those are my own brotli -q 11 figures over the saved bodies; the CDN's encoder is less aggressive, and when I measured the server's own brotli over the same 72 pages the whole-site total came out at 809 KB rather than 638 KB. Second, compressed bytes are not the only cost: those 581 elements are also 581 nodes the parser builds and the layout engine measures, and they appear again in the React flight payload for the same route.

So the honest conclusion is the boring one. A sprite sheet with <use href="#arrow-right"> would delete 193 KB of source and save about 12 KB across 72 page loads, and would cost us a second request, a cache entry to invalidate, and the ability to colour each icon with currentColor from the class beside it. We are not doing that. The thing that looks like waste, measured, is a rounding error.

Where they are not

Three of the 72 pages contain zero icons: the privacy policy, the terms and the cookie page. They are the three pages that get the compact footer, with no navigation columns and therefore no chevrons, and their bodies are plain prose with no cards and no arrows.

At the other end, the homepage has 46 and the pricing page has 29. The comparison pages sit at 23 to 25 each, because every row of a comparison table carries two state markers.

The half that is not about bytes

Of the 581 icons:

  • 489 carry aria-hidden="true"
  • 92 carry an aria-label
  • 0 carry role="img"
  • 0 contain a <title> element

There are exactly two attribute shapes in the whole site, and the 92 labelled ones are all the same thing: the Yes, Partly and No markers in the comparison tables.

function StateIcon({ state }: { state: ComparisonRow["us"]["state"] }) {
    if (state === "yes") return <Check className="..." aria-label="Yes" />
    if (state === "no") return <X className="..." aria-label="No" />
    return <Minus className="..." aria-label="Partly" />
}
Enter fullscreen mode Exit fullscreen mode

An aria-label on a bare <svg> with no role is the pattern every accessibility checklist tells you not to write, on the grounds that the label may not be exposed. So I checked rather than assumed, by reading Chromium's real accessibility tree for the paper versus app comparison over the DevTools protocol:

Accessibility.getFullAXTree
  total nodes: 489
  nodes with role "image": 19
    { role: 'image', name: 'PubTrivia logo', ignored: false }
    { role: 'image', name: 'Yes',  ignored: false }
    { role: 'image', name: 'No',   ignored: false }
    { role: 'image', name: 'Partly', ignored: false }
    ...
Enter fullscreen mode Exit fullscreen mode

That page has 25 icons in the DOM: 18 labelled, 7 hidden. Nineteen image nodes come back, the 18 labels plus the logo, every one named and none ignored. The seven aria-hidden icons are pruned out of the tree entirely, which is the whole point of them.

So in this engine the labels work. role="img" is still the safer spelling for older WebKit, and it is two characters per icon, so that one I will take.

The more useful thing the census surfaced is that the labels are not load-bearing anyway. Every marker in those tables sits next to a sentence:

<div class="flex gap-2">
  <svg ... aria-label="Yes"></svg>
  <span class="text-muted-foreground">...the sentence that explains the row...</span>
</div>
Enter fullscreen mode Exit fullscreen mode

A cell is never a tick on its own. The tick is the glanceable summary of a sentence that is already there, which means the accessibility story for those tables was decided by the three-state design, not by an ARIA attribute. That is the part worth copying.

What I am changing

  1. role="img" on the 92 labelled icons. Cheap, and it removes the only line in this audit that an auditor would flag.
  2. Nothing about inlining. 12.8 KB across the whole site is not a problem to solve.
  3. Nothing about xmlns either, even though it is 19.9 KB of dead weight, because it comes out of lucide-react and patching a dependency to save 1.5 KB compressed is how you end up with a patch file nobody dares delete.

Check it yourself

No account needed, and every number above is in the HTML:

# how many icons on a page, and how many are hidden from assistive tech
curl -s https://pub-trivia.app/compare/paper-vs-app-quiz | grep -o '<svg' | wc -l
curl -s https://pub-trivia.app/compare/paper-vs-app-quiz | grep -o 'aria-hidden="true"' | wc -l
curl -s https://pub-trivia.app/compare/paper-vs-app-quiz | grep -o 'aria-label="[^"]*"' | sort | uniq -c

# the three pages with no icons at all
curl -s https://pub-trivia.app/legal/cookies | grep -c '<svg'

# the most repeated instruction on the site
curl -s https://pub-trivia.app/ | grep -o 'd="M5 12h14"' | wc -l
Enter fullscreen mode Exit fullscreen mode

For the accessibility half, open any comparison page, open the DevTools accessibility pane, and click one of the ticks: it reports as an image named "Yes". Then read the sentence next to it, which is the reason the tick does not have to carry the row on its own.

If you want to see the icons doing their actual job, the round planner and the other free tools are the busiest pages for them and need no account.

Top comments (0)