Images as Base64 Data URIs, and When Inlining Helps or Hurts

Sometimes an image has to live inside a text file. A single-file HTML report, a CSS file with a small icon in it, a JSON test fixture or a config that can’t point at a URL. The usual answer is a Base64 data URI, and the usual mistake is using one for a picture that should have stayed a normal file. I built the Image to Base64 tool for the first case, and most of the work went into making it warn you about the second.

What a data URI is

A data URI puts a file’s contents where a URL would normally go. For an image it looks like data:image/png;base64,iVBORw0KGgo..., in three parts. data: says the content is right here, not on a server. image/png is the media type, which tells the browser how to decode what follows. ;base64, says the rest is Base64 text. Anything that accepts an image URL accepts this instead: the src of an <img>, a CSS url(...), the href of a favicon link.

The browser reads the media type from the URI, not from a file name, so it has to be right. The tool doesn’t build it by hand. It reads each file with the browser’s FileReader.readAsDataURL, which writes in the type the browser already detected for the file. That’s why an SVG comes out as data:image/svg+xml;base64,... and a GIF as data:image/gif;base64,... with no extra code.

Why the text is a third bigger

Base64 turns every 3 bytes (24 bits) into 4 characters of 6 bits each, picked from 64 safe characters: A–Z, a–z, 0–9, + and /. When the file length isn’t a multiple of 3, the last group is padded with =. So the length is easy to predict:

export const base64Length = (bytes: number) => 4 * Math.ceil(bytes / 3)

The test for this compares it with Node’s own Base64 encoder for files of 0, 1, 2, 3, 4, 100 and 1,000 bytes, so the padding cases are covered.

Here’s what that means for real sizes, before the short data:image/...;base64, prefix is added:

Image file Base64 text
3 KB (3,072 bytes) 4,096 characters
10 KB (10,240 bytes) 13,656 characters
50 KB (51,200 bytes) 68,268 characters
300 KB (307,200 bytes) 409,600 characters

Every character is one byte in an HTML or CSS file, so the inlined image costs about 33% more than the file it came from. The tool shows both numbers next to each image (“3.0 KB image → 4.0 KB of text”), so you see the cost before you paste it anywhere.

Four ways to paste the same image

People need the result in different places, so the tool offers four formats. They all start from the same data URI, and switching between them is a few lines of string work:

export function formatBase64(dataUri: string, format: Base64Format): string {
  switch (format) {
    case 'data-uri': return dataUri
    case 'base64': return dataUri.slice(dataUri.indexOf(',') + 1)
    case 'html': return `<img src="${dataUri}" alt="">`
    case 'css': return `background-image: url("${dataUri}");`
  }
}
  • Data URI is the full string, for any place that takes a URL.
  • Base64 is only the part after the comma. It’s what APIs and JSON fields usually want, because they store the media type in a separate field.
  • HTML is a ready <img> tag. The alt is empty on purpose, because only you know what the image shows. Fill it in, or leave it empty if the image is decoration.
  • CSS is a background-image rule you can drop into a selector.

Because the format is applied only when you copy or download, changing it doesn’t read the file again. You can add a batch of images, pick “CSS”, copy each one, then switch to “HTML” for the next job.

When inlining helps

A data URI saves the browser a separate request for the image. That’s the whole benefit, and it matters most when the image is tiny and needed right away:

  • small icons and logos in CSS that every page uses;
  • images inside a file that has to stand alone, such as an HTML report someone opens from their downloads;
  • test fixtures and JSON files where a real file path would be awkward;
  • placeholders in a prototype, before there’s anywhere to host the real image.

When it hurts

The cost grows with the image, and it shows up in three ways:

  • Size. The 33% growth is paid every time. Gzip or Brotli compression on the server wins back some of it, but not all of it.
  • Caching. A normal image file is cached once and reused by every page that shows it. An inlined image is downloaded again with every HTML page that contains it. Inside a CSS file it’s cached with the CSS, which is why small icons in a stylesheet are the classic good case.
  • Blocking. A large data URI in your HTML or CSS has to arrive before the rest of that file can be read. A normal <img> loads alongside the page instead.

That’s where the tool’s warning comes from. Any image over 50 KB gets a note saying that Base64 works best under about 10 KB, and that bigger images load faster as normal files. The tool still converts it, because you may have a good reason. It just doesn’t let you do it without knowing.

Limits I set in the tool

A few constants decide how the tool behaves with big files:

const MAX_BYTES = 10 * 1024 * 1024     // Base64 text this big already freezes editors
const PREVIEW_CHARS = 20_000           // longer strings are shortened on screen, copied in full
const LARGE_WARNING = 50 * 1024
  • 10 MB per file. A 10 MB image becomes about 13.3 MB of text. Code editors and browser text boxes already struggle at that length, and there’s no sensible place to paste it.
  • 20,000 characters on screen. A text box holding a few million characters makes the whole page slow to scroll and select. Longer results are cut on screen with a note, but Copy and Download .txt always use the full text.
  • 50 KB warning, explained above.

Two smaller details came from testing. If the browser blocks clipboard access, the tool says so and suggests selecting the text by hand or downloading it, instead of failing silently. And iPhone HEIC photos are sent to the HEIC to JPG converter first. Most browsers can’t display HEIC, so a data:image/heic URI would show nothing on most of the screens that load it.

What Base64 doesn’t change

Base64 is an encoding, not a conversion. The bytes inside the data URI are exactly the bytes of your file, with no re-compression and no change in quality. That has two practical sides:

  • Make the file small first. Every byte you save removes about 1.33 characters of text. For a PNG, run it through Compress Image before you encode it; for an SVG, remove editor metadata and unused definitions.
  • Hidden data comes along. A photo straight from a phone often carries its GPS location and camera details in its EXIF data. Base64 copies those too, into your HTML, where anyone can decode them. For photos, use Remove EXIF data first. The tool reads files locally and doesn’t upload them, so what ends up in the text is only what you paste somewhere yourself.

One honest limit: for SVG, a URL-encoded data URI (the SVG text itself, with a few characters escaped) is often shorter than Base64, because SVG is already text. This tool always produces Base64, which works everywhere but isn’t always the shortest option for SVG.

Try it

The Image to Base64 converter takes SVG, PNG, JPG, WebP and GIF files, one or many at a time, and gives you a data URI, raw Base64, an <img> tag or a CSS rule for each. More tools for preparing images are on the image tools page.

Tools in this article

  • Convert an image to Base64

    Convert images to Base64 in your browser. Copy a data URI, raw Base64, an HTML img tag or a CSS background rule. No upload.

More articles