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:
- Priority / queue — competes with CSS, fonts, and your first API call
- Cache miss on first visit — every new deploy with hashed filenames resets the miss
- CORS / CSP footguns — SVG-as-
<img>vs inline SVG vs background-image all behave differently - 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; } or:
const COPY_ICON = "data:image/svg+xml," + encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>`); 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:
- Prefer SVG over PNG when the mark is flat UI chrome
- Strip editor junk (
id, unuseddefs, Illustrator cruft) before encoding - Prefer
encodeURIComponentfor SVG (not base64) so the string stays readable in diffs - Cap size — if the data URI is longer than the component that uses it, reconsider
- 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
currentColorSVG 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.