DEV Community

Sherotree
Sherotree

Posted on

Tiny icons as data URIs — skip another network hop for chrome assets

Chrome UI is full of tiny assets that should never fight the critical path: a 16×16 chevron, a mute glyph, a spinner frame, a toolbar mark that shows up before the rest of the page finishes deciding who you are.

I still see those shipped as separate /icons/*.svg or /icons/*.png requests. On a good day they ride HTTP/2 multiplexing and you barely notice. On a cold CDN edge, a flaky hotel Wi‑Fi, or a settings drawer that mounts before the icon sprite is warm, you get the classic flash of empty boxes.

For chrome icons — not hero art, not user avatars — I default to inlining as data URIs more often than I used to.

What I mean by "chrome assets"

Not the marketing hero. Not a product screenshot. The boring bits that make the shell feel finished:

  • Sidebar / toolbar glyphs under ~2–4 KB each
  • Empty-state illustrations that are basically icons with padding
  • Status dots and badges that are vector or tiny PNG
  • Favicon-sized marks reused inside the app chrome

If an asset is decorative, tiny, and shown before meaningful paint, a network hop is a tax. If it's large, user-generated, or rarely shown, leave it as a normal URL.

The network hop you forget about

A separate icon URL costs more than bytes:

  1. Priority / queue — competes with CSS, fonts, and your first API call
  2. Cache miss on first visit — every new deploy with hashed filenames resets the miss
  3. CORS / CSP footguns — SVG-as-<img> vs inline SVG vs background-image all behave differently
  4. Layout flash — width/height reserved late → CLS or empty toolbar buttons

Data URIs don't magically shrink bytes (base64 costs ~33%), but they remove a round trip and make the asset travel with the CSS/JS that needs it. For a 600-byte SVG that's often the right trade.

When data URIs win (and when they don't)

Prefer data URI when:

  • The icon is ≤ ~2–3 KB uncompressed (SVG preferred)
  • It appears in first paint / shell / authenticated chrome
  • You're already shipping a CSS or JS module that owns that surface
  • You don't need the browser to cache the same glyph across many unrelated pages independently

Prefer a normal URL (or sprite) when:

  • The asset is large or shared across many independent entry points
  • You need CDN edge caching and long-lived immutable URLs
  • Design iterates the icon weekly and you don't want CSS churn
  • You're shipping dozens of icons and a sprite / icon font / SVG sprite sheet is cleaner

I don't inline a 40 KB illustration. I do inline the 800-byte "copy" glyph next to every code block toolbar.

A workflow that doesn't fight the design handoff

Design still exports PNG/SVG. Engineering still needs a string you can paste into CSS:

.toolbar-copy {
  background-image: url("data:image/svg+xml,...");
  background-size: 16px 16px;
}
Enter fullscreen mode Exit fullscreen mode

or:

const COPY_ICON =
  "data:image/svg+xml," + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>`);
Enter fullscreen mode Exit fullscreen mode

The friction is converting the export into a safe data URI without opening three tabs and hoping the encoder didn't mangle # / quotes.

Checklist I use:

  1. Prefer SVG over PNG when the mark is flat UI chrome
  2. Strip editor junk (id, unused defs, Illustrator cruft) before encoding
  3. Prefer encodeURIComponent for SVG (not base64) so the string stays readable in diffs
  4. Cap size — if the data URI is longer than the component that uses it, reconsider
  5. Keep width/height in CSS so you don't rely on intrinsic size for layout

Converting without another upload site

I don't want to POST a brand mark to a random "image to base64" site just to get a data: string. Same Offline litmus as other one-off transforms: if convert dies with DevTools Offline, it wasn't local enough for me.

When I need a quick tab for this, I've used Uwarp's image → data URI tool — drop the icon, get a data URI, paste into CSS/JS. Worth trying when the alternative is a desktop converter or an upload encoder you can't audit.

Roll your own if this is daily: a tiny Node script with fs.readFileSync + encodeURIComponent is fine. The point is the habit (inline tiny chrome) more than the tool.

Don't turn this into a religion

A few failure modes I hit:

  • Inlining everything → CSS balloons; first paint gets worse, not better
  • Base64 PNG for every glyph → you paid the 33% tax and still ship raster blur on retina
  • Inlining user avatars → privacy + size disaster; never do this
  • Forgetting dark mode → one data URI, one color; prefer currentColor SVG or dual tokens

Rule of thumb: if I can't justify the icon as "shell chrome under a few KB," it stays a URL.

Tiny habit

Next time you add a toolbar button and reach for /assets/icons/copy.svg, ask: is this a critical-path chrome glyph? If yes, inline it as a data URI (SVG + encodeURIComponent preferred). If no, leave the network hop alone.

Fewer empty boxes on first paint. Fewer "why is the mute icon late?" threads. Same design file — just a better shipping shape for the tiny stuff.

Top comments (0)