Base64 encode/decode · Guide

Base64 Encode an Image: Data URIs, Size Cost and When to Use Them

Encoding an image to Base64 turns its bytes into a text string you can paste straight into HTML, CSS, JSON or an email template — no separate file and no extra request. The convenience is real and so is the cost, and the line between them is sharper than most advice suggests. Here is where it falls and why.

From image to data URI

Encoding produces the raw Base64 payload. To use it in a browser you wrap it as a data URI, which needs the MIME type and the base64 marker.

HTML
data:[<mime-type>][;base64],<payload>

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUg..." alt="Logo">

.badge {
  background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxu...');
}

The MIME type must match the actual format. Labelling a JPEG as image/png makes some browsers refuse to render it, and the failure is a blank element with nothing in the console.

SVG is the exception worth knowing. Because it is already text, Base64 is the wrong tool — percent-encoding is both smaller and readable, and it lets you edit colours in place rather than re-encoding.

CSS
/* Base64: opaque and ~33% larger than the source */
url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0i...');

/* Percent-encoded: smaller and still editable */
url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg"...');

Encoding it, on any platform

Shell
# macOS / BSD — no line wrapping by default
base64 -i logo.png | pbcopy

# GNU/Linux — -w0 stops it wrapping at 76 columns
base64 -w0 logo.png

# straight to a usable data URI
echo "data:image/png;base64,$(base64 -w0 logo.png)"
Python
# Python
import base64, pathlib
raw = pathlib.Path('logo.png').read_bytes()
uri = 'data:image/png;base64,' + base64.b64encode(raw).decode()
JavaScript
// Browser — from a file input
const toDataUri = (file) => new Promise((res) => {
  const r = new FileReader();
  r.onload = () => res(r.result);   // already a full data: URI
  r.readAsDataURL(file);
});

// Node
const uri = 'data:image/png;base64,' +
  require('fs').readFileSync('logo.png').toString('base64');

Line wrapping is the classic trap. GNU base64 wraps output at 76 characters, and those newlines make the data URI invalid — -w0 is not optional. The MIME email variant of Base64 wraps deliberately, which is why a string copied out of an email source often needs its newlines stripped before it will render.

The 33% penalty, precisely

Base64 encodes three bytes as four characters, so the payload is 4/3 of the original — about 33% larger — plus padding and the data:image/png;base64, prefix.

Text
  4 KB icon   ->  ~5.4 KB
 40 KB logo   ->  ~53 KB
300 KB photo  -> ~400 KB

Gzip or Brotli recovers some of that, because Base64 of compressed image data still has enough redundancy to compress a little — but not much. Expect to get back a few percent, not the 33%.

The size is only half the cost. An embedded image sits inside your HTML or CSS, so it cannot be cached separately, cannot be lazy-loaded, and is re-downloaded in full on every page that includes it. Put a 50 KB logo in a stylesheet and every visitor downloads it again whenever that stylesheet changes.

There is a rendering cost too. A data URI in CSS blocks the stylesheet from parsing until it is read, and a large one measurably delays first paint — the opposite of what embedding was meant to achieve.

When embedding is the right call

The case for a data URI is strongest when the request overhead exceeds the size penalty, and when separate caching would not have helped anyway.

**Small, always-visible assets.** Icons, a 1px gradient, a tiny logo. Under roughly 5 KB the saved round trip usually wins, though this argument is weaker over HTTP/2 and HTTP/3 where concurrent requests are cheap.

**Email templates.** Many clients block external images by default, so embedding is often the only way an image appears at all. Gmail is the notable exception — it strips data URIs in <img>, which is why most bulk senders still host images and accept the blocking.

**Single-file documents.** An HTML report that must work offline, or be emailed as one attachment, has no alternative.

**Test fixtures and mock data.** A known-good image inline in a test is easier to reason about than a fixture file, and it cannot go missing.

For photos, hero images, or anything a user might see once, keep the file separate. Caching, a CDN and modern formats such as WebP or AVIF beat embedding comfortably.

Content Security Policy

Data URIs interact with CSP, and this catches people out after the fact — the images work in development and disappear when a policy is added.

HTTP
Content-Security-Policy: img-src 'self' data:;

Without data: in img-src, every embedded image is blocked. The same applies to font-src for embedded fonts and to style-src when a data URI appears in an inline style.

Allowing data: broadly is not free: it weakens the policy, because a data URI can carry arbitrary content. For img-src the risk is modest; for script-src allowing data: is genuinely dangerous and should never be done.

Going the other way

To decode a data URI back to a file, strip everything up to and including the comma, then decode the remainder as bytes.

Shell
# from a data URI in a file
sed 's/^data:[^,]*,//' uri.txt | base64 -d > out.png

# verify what you actually got
file out.png
# out.png: PNG image data, 512 x 512, 8-bit/color RGBA

file is worth running. If it reports something other than an image, the payload was truncated or the MIME type in the URI was wrong — both produce the same blank preview.

Truncation is the most common failure. Data URIs copied from browser DevTools are often cut off, because the panel elides long values. Compare the string length against the expected size: Base64 length should be about 4/3 of the file size, and a valid payload's length is always a multiple of four once padding is included.

Shell
# a valid payload's length is divisible by 4
python3 -c "print(len(open('payload.txt').read().strip()) % 4)"   # 0

Frequently asked questions

How much bigger does Base64 make an image?

About 33%. Base64 represents three bytes as four characters, so a 40 KB file becomes roughly 53 KB before the data:image/png;base64, prefix. Gzip or Brotli recovers a few percent, not the whole overhead.

Do embedded images hurt page speed?

Usually yes, above a few kilobytes. The payload cannot be cached separately, cannot be lazy-loaded, and is re-downloaded whenever the containing HTML or CSS changes. A large data URI in a stylesheet also delays parsing and therefore first paint. Under about 5 KB the saved request can still be worth it.

What MIME type should I use?

The one matching the actual file: image/png, image/jpeg, image/gif, image/webp, image/svg+xml. A mismatch makes some browsers refuse to render the image with no console error — a silently blank element.

Why is my data URI invalid or blank?

Most often line wrapping or truncation. GNU base64 wraps at 76 characters by default and those newlines break the URI — use base64 -w0. Data URIs copied from DevTools are frequently elided; check that the payload length is roughly 4/3 of the file size and divisible by four.

Should I Base64 encode SVG?

No. SVG is already text, so percent-encoding produces a smaller string than Base64 and keeps the markup readable and editable in place. Reserve Base64 for binary formats.

Why do my data URI images disappear under CSP?

The policy needs data: in the relevant directive — img-src 'self' data: for images, font-src for embedded fonts. Note that allowing data: weakens the policy, and it must never be added to script-src.

Ready to try it?

Open the free browser-based Base64 encode/decode and apply what you just read — no sign-up, runs locally.

Open the Base64 encode/decode tool