Image to Base64: Embed Images in HTML and CSS
Two claims dominate most advice about inlining images: "Base64 makes the file a third bigger" and "inlining cuts requests and speeds up loading." The first holds in only one of three ways you can measure it, and the second stopped being reliable once HTTP/2 became the norm. What actually decides whether an image should be inlined is not how big it is, but three quantities rarely discussed together: who gets to cache it, where its bytes sit on the render path, and how many pages will repeat it. This guide separates those three costs and ends with a threshold you can apply directly.
Every measurement below is reproducible with Node's built-in zlib against real files in this repository. The paths and byte counts are given as we go, so nothing here rests on a claim you have to take on faith.
1. Inlining Trades One Request for Three Bills
Writing an image as a data URI replaces a network request with bytes: the browser no longer opens a request for that URL and waits for a response, but the bytes must now arrive with the host document. That trade carries three separate costs, and most articles only ever discuss the first.
Size. Base64 turns every 3 bytes into 4 characters, which makes the file longer. Source files live in repositories and on disks; their size has a cost, but it is not a cost your visitors pay.
Caching. An external image has its own URL, its own cache entry, and its own expiry. An image inlined into HTML has no identity of its own, so when the host document expires, it is downloaded again along with it.
Path. An external image can be lazy-loaded, given a download priority, or swapped for a different resolution per screen width. Bytes written into an HTML or CSS file have already been downloaded by the time the browser reads them, so none of those levers apply.
Of the three, size is quoted the most and influences the decision the least. The next section shows why with measured numbers.
2. One Image, Three Measurements: File Size, Transfer Size, Parse Size
"33% bigger" describes the file on disk. Files on disk are not billed individually; transfer size is, and HTML and CSS are almost always served compressed. A compressor can do more with Base64 text than most people expect: each Base64 character carries 6 bits of information while occupying 8, and that two-bit redundancy is exactly what compression reclaims.
The figures below were measured locally with Node 22's zlib. The payload is incompressible random bytes, standing in for an image that is already compressed, as PNG, JPEG and WebP all are. The host document is the blog page you are reading:
| Original image size | Data URI length | Uncompressed transfer delta | gzip transfer delta | brotli transfer delta |
|---|---|---|---|---|
| 1 KB | 1,390 chars | +1,390 bytes | +1,321 bytes | +1,070 bytes |
| 4 KB | 5,486 chars | +5,486 bytes | +4,770 bytes | +4,174 bytes |
| 8 KB | 10,946 chars | +10,946 bytes | +9,127 bytes | +8,287 bytes |
| 20 KB | 27,330 chars | +27,330 bytes | +21,823 bytes | +20,646 bytes |
| 100 KB | 136,558 chars | +136,558 bytes | +104,342 bytes | +102,545 bytes |
Put the transfer delta over the image's own byte count and the result is counter-intuitive: a 1 KB image costs 1.29 times its weight to send over gzip, a 4 KB image 1.16 times, a 20 KB image 1.07 times, and a 100 KB image only 1.02 times, or 1.001 times with brotli. The "one third heavier" claim holds only in the low-hundreds-of-bytes range, and it holds less the larger the image gets.
Compressible images invert the picture entirely. assets/favicon.svg in this repository is 892 bytes, or 1,192 characters once encoded, but the original gzips to 418 bytes while that encoded text still gzips to 700, an increase of 67% on the wire. Vector art and uncompressed bitmaps already compress well; Base64 first turns them into hard-to-compress 6-bit symbols, which means giving up compression you were getting for free. It is also the reason vector icons should be inlined as SVG markup rather than converted to Base64.
3. Estimating the Threshold: Convert a Request into Bytes
If transfer size is not the deciding factor, the threshold cannot be derived from size alone. It comes from asking what one request is worth in bytes, and the conversion is a single line: Base64 produces one extra character for every three bytes of source data, so 1 KB of request overhead equals about 3 KB of image bytes.
What does an extra request actually cost? Once a connection is reused under HTTP/2 with header compression in play, what remains is the part of the request headers that cannot be compressed, such as cookies, plus the response headers. That is a few hundred bytes to a little over 1 KB. Working backwards, the byte threshold lands at roughly 3 KB.
Build tools have converged on the same neighbourhood. Vite's build.assetsInlineLimit defaults to 4,096 bytes, and webpack 5's asset modules default to 8 KB. A 4,096-byte image becomes 5,464 characters, an increase of 1,368 bytes; an 8,192-byte image adds 2,732 bytes. Both are the same order of magnitude as the header cost of one image request that carries cookies, so the defaults were not arbitrary.
Consider when they were chosen, though. Under HTTP/1.1 a browser opened six connections per origin and the seventh image queued for a full round trip, so request count really was the bottleneck. Multiplexing removed that bottleneck, and header compression cut the per-request header cost from about a kilobyte to tens of bytes. The right threshold today is lower than those defaults, not higher. Copying 4 KB or 8 KB unchanged mostly results in assets that should have been cached independently being folded into the document instead.
4. Four Worked Examples
Example one: a 4 KB icon, right at the line. 4,096 bytes becomes 5,486 characters including the 22-character data:image/png;base64, prefix, which is 1,390 extra bytes in the source file and 4,770 extra bytes over a gzip connection. In exchange, one request disappears. If the icon appears on a single page and the visitor comes once, that is a good trade.
Example two: the same icon on 40 pages. This is where inlining quietly loses. Referenced as a file, a visitor downloads 4 KB once and every one of those 40 pages hits cache afterwards. Inlined, each page carries its own 5,486 characters, so a visitor who reads six pages downloads the same icon six times, 32,916 bytes in total, eight times the file-based cost. The gap is noise at a few kilobytes and ruinous at a hundred. That is why "how many pages share this asset" deserves to be asked before "how big is it."
Example three: twelve icons inlined into one stylesheet. At 5 KB each that is 81,936 bytes, about 80 KB, sitting inside a render-blocking stylesheet that is content-hashed for long-term caching. Change one pixel of one icon and the whole stylesheet's hash changes, so every returning visitor re-downloads all 80 KB instead of a single 5 KB icon: a 16 times penalty for a one-line change. Build tools will inline anything below the threshold, but no tool can tell you whether an icon is likely to change.
Example four: a 57 KB social preview image. assets/og-image.png is 57,954 bytes and encodes to 77,272 characters, a factor of 1.3333. Its transfer cost is nearly nothing: 56,004 bytes gzipped as a file versus 56,401 bytes gzipped as Base64, a difference of 397 bytes, or 0.7%. It should still never be inlined, because it is shared across pages, can be cached indefinitely, and never belongs on the critical path. Folding it into any HTML document would add 57 KB to every response carrying that document. Example four is the entire argument in one file: the deciding factor is caching and reuse, not size.
5. Alternatives That Usually Beat Base64
Before reaching for a data URI, confirm that something simpler does not already solve the problem. These four approaches cover almost every case where the goal is simply to avoid an extra request.
| Approach | Best for | How it differs from an inlined data URI |
|---|---|---|
| SVG sprite | A full set of UI icons | Every icon in one file: one request, one cache entry, and one file invalidated per icon change; symbols inherit currentColor and follow the surrounding text colour |
| Inline SVG markup | A handful of vector icons | No encoding step; the markup compresses along with the document and stays resolution-independent |
| Icon font | Legacy projects | Small, but needs subsetting, has a window where glyphs are missing, and needs text alternatives for accessibility |
| Separate file with a long cache | Photos, hero images, and anything bulky | No size penalty, independently invalidated, lazy-loadable, and swappable per screen width |
This site is its own counter-example. Every interface icon lives in one SVG sprite: 53,475 bytes, 115 symbols, 465 bytes per icon on average, which is 7,469 bytes gzipped and 6,310 bytes with brotli. One request, one cache entry, and colour inherited from the surrounding text. Convert those 115 icons into 1 KB Base64 bitmaps instead and every page carries an extra 154 KB of icons whose colours are baked into pixels, requiring a second set for dark mode. That is the payoff of not using Base64, stated in bytes.
Two situations remain where Base64 is genuinely the only option. The first is a channel that accepts nothing but text, such as a JSON API that must carry a small image, or a single-file HTML deliverable that cannot reference external resources. The second is a host environment that forbids external references, as some email templates do. Even there, stay cautious: support for data: images varies across major email clients and several strip them outright, so a template has to be verified in a real client rather than a local preview.
The encoding step itself is straightforward. Hand an image to the Image to Base64 tool and it returns a string you can paste directly; for plain text, use the Base64 encoder and decoder.
6. Four Knock-On Problems Nobody Mentions
A CSP without data: breaks inlined images silently. Inline and external images are governed by the same img-src rule, but only the inline one depends on the data: scheme. Tighten a policy by removing data: from img-src and external images keep working while inlined ones become broken images, with nothing outside the console to explain it. This site keeps both data: and blob: in img-src, which is a deliberate permission rather than a default.
Inlined images cannot be lazy-loaded. A data URI in the HTML arrives with the document whether it sits above the fold or twenty screens down, and whether or not the visitor scrolls that far. Ten inlined thumbnails inside a collapsed panel are paid for in full by every visitor; as external files with loading="lazy" they cost nothing until the panel opens.
Inlined images cannot be responsive. One data URI serves every device, so a phone receives exactly the bytes a high-density desktop does. Serving retina-sharp inlined icons means embedding two or three times the pixels and charging every device for them, whereas srcset lets the browser choose. That trade-off disappears once the bytes are inline.
Colour and theme are frozen into the bytes. A decoded Base64 bitmap has fixed pixels that CSS cannot recolour, so dark mode, a rebrand, or an inverted hover state each need another inlined copy. Vector approaches inherit currentColor, so one asset serves every theme.
7. Frequently Asked Questions
- Do Base64 images actually load faster?
- Not necessarily, and it depends on which part you count as faster. Inlining saves a round trip and one request's fixed overhead, which is a genuine win on a cold cache when the image is small and appears once. But the byte count does not shrink, and over a compressed connection it rises slightly. Once an image is reused across pages or grows beyond a few kilobytes, the repeated bytes outweigh the delay you saved.
- How large does an image have to be before it should not be inlined?
- A size threshold is a shortcut, not the answer. The common 4 KB default comes from the fact that one request's header overhead equals roughly 3 KB of encoded image bytes. The reuse question matters more: any image that appears on multiple pages or might later be replaced should stay external no matter how small it is. Reserve inlining for one-off, very small assets you are confident will never change.
- What goes wrong with inlined CSS background images?
- Three things. The stylesheet is render-blocking, so every inlined byte lands on the critical path to first paint. Changing one inlined background invalidates the whole stylesheet's hash, making returning visitors re-download all of it. And if that stylesheet is shared across pages while only one page actually uses the image, every other page pays for it too.
- Does inlining make an image blurry?
- The encoding does not. Base64 is reversible, and the decoded bytes match the file you started with exactly. Blurriness usually comes from a different step: to keep the inlined size acceptable you compressed the image or scaled it down, and the quality loss belongs to that step. Converting vector art to a Base64 bitmap is the real image-quality loss, because it degrades from sharp at any resolution to a fixed set of pixels.
- How much should be inlined for a single-file HTML deliverable?
- In that situation inlining is the only option, so the focus shifts to total weight. Compress first, encode second, and never the other way round: run images through an image compressor to the target size, then convert. Prefer vector wherever possible, because vector survives document compression while a bitmap is already-compressed bytes being inflated. If an offline document needs dozens of images, a separate archive is usually better than inlining them all.
8. Four Common Misconceptions
Misconception one: image size is the only criterion. Rules like "inline anything under 10 KB" answer the size question and skip the reuse question. A 4 KB icon used on 40 pages makes every visitor download it repeatedly; a 57 KB social preview used in one place costs almost nothing in transfer. Ask about reuse first, then about size.
Misconception two: the build tool default is settled industry practice, so copy it. The 4 KB and 8 KB thresholds date from HTTP/1.1, when browsers limited connections per origin to single digits and requests queued behind each other. Multiplexing lets requests run in parallel and header compression cut per-request headers from about a kilobyte to tens of bytes, so the crossover moved down. Copying the default mostly results in resources that should have been cached separately being folded into the document.
Misconception three: Base64 is 1.33 times the size, so the page is a third heavier. This conflates file size with transfer size. What Base64 adds is two bits of representation redundancy per character, and compression reclaims most of it: measured on 100 KB of incompressible data, the gzip transfer delta is 1.9% and the brotli delta is 0.1%. What genuinely makes pages heavier is the same asset being inlined over and over across many pages.
Misconception four: if compression absorbs the overhead, inlining large images is fine. That reasoning forgets caching. The transfer measurement only shows that the first download is not noticeably more expensive; it does not address the second download, which could have been free. Compression recovers the cost of encoding. It does not recover the cost of losing a cache entry.
9. Practical Recommendations
- Ask about reuse before size: keep anything that appears across pages, or that might be replaced later, as an external file, and inline only one-off, very small assets.
- Keep vector work vector: build icons as an SVG sprite or inline the SVG markup directly rather than converting to Base64 bitmaps, which throws away both compression and theming.
- Lower the threshold below the default: the 4 KB and 8 KB defaults come from the HTTP/1.1 era, and multiplexing plus header compression push the real crossover lower. Measure the actual character count with the Base64 encoder and decoder before deciding.
- Always compress before encoding: inlined size scales with source size, so pass the image through an image compressor first, or resize it with the image resize and crop tool when you need exact pixel dimensions. To compare how formats trade size against quality, see the image format conversion guide.
- Check the CSP first: confirm that the site's img-src includes
data:before shipping inlined images, or you will get silent broken images instead. - Let the build pipeline do the inlining: automatic threshold-based inlining is far more reliable than pasting long encoded strings by hand, since a hand-pasted blob can be neither reviewed nor searched. For the encoding background itself, read the Base64 encoding guide.