DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

Image to data URI: one FileReader call, a string split, and the ~33% base64 tax that decides whether to inline

A browser can render an image straight from a string instead of a URL. A data: URI like data:image/png;base64,iVBORw0KGgo… carries the whole file inline as base64 text — paste it into background-image: url(…) or an <img src> and there is no separate network request, the bytes travel with your CSS or HTML. I built a tool that turns a dropped image into one, shows a live preview, the full byte math, and a verdict on whether inlining is a good idea. It has no server and no library, because it turns out it doesn't need one. Here is what I learned building it.

The whole engine is one browser call

The instinct is to reach for an encoder. You don't need one. FileReader.readAsDataURL(file) reads the file's bytes in the browser and hands back a finished data: URI — MIME type sniffed, base64 done — on the async onload. Nothing is uploaded, which is exactly why a converter like this needs no backend.

function handleFile(file){
  if (!file || !file.type.startsWith("image/"))
    return showError("That's not an image file.");
  const reader = new FileReader();
  reader.onload  = e => render(file, e.target.result); // result = the data URI
  reader.onerror = () => showError("Could not read the file.");
  reader.readAsDataURL(file);   // async → onload fires with the string
}
Enter fullscreen mode Exit fullscreen mode

Everything after that is string slicing and one division.

Splitting the URI is one indexOf

A data URI is data:<mediatype>[;base64],<data>. Everything before the first comma is the header (scheme + MIME + encoding flag); everything after is the payload. One indexOf(",") and two slices, no parser.

function parseDataURI(uri){
  const comma   = uri.indexOf(",");
  const header  = uri.slice(0, comma);     // "data:image/png;base64"
  const payload = uri.slice(comma + 1);    // "iVBORw0KGgoAAAANS..."
  const mime    = header.slice(5, header.indexOf(";")); // "image/png"
  return { header, payload, mime };
}
Enter fullscreen mode Exit fullscreen mode

The ~33% overhead, from first principles

This is the number the tool exists to make visible. Base64 regroups bits: it takes 3 bytes (24 bits) at a time and re-splits them into 4 groups of 6 bits, mapping each to one of 64 safe characters. So it always emits 4 characters for every 3 input bytes, and since each character is one byte of text, the encoded size is 4/3 of the original — about 33% bigger. The original size is just file.size; the base64 size is payload.length because ASCII is one byte per char.

function byteStats(file, payload){
  const orig   = file.size;               // raw bytes
  const b64    = payload.length;          // 1 ASCII char = 1 byte
  const theory = 4 * Math.ceil(orig / 3); // 3 bytes -> 4 chars
  const overheadPct = (b64 - orig) / orig * 100;
  return { orig, b64, theory, overheadPct };
}
// e.g. 900 B image -> 1200 B base64 -> +33.3%
Enter fullscreen mode Exit fullscreen mode

The subtlety worth stating out loud: this is growth, not shrinkage. Base64 is an encoding, not compression. It exists so binary bytes survive in text-only places like a CSS file or a JSON field. If you want smaller, compress the image first, then encode.

When to inline — the actual judgment

The point of the tool isn't the conversion, it's the verdict. Inlining is a trade. It wins for small, single-use assets — icons, bullets, tiny SVGs under a few KB — where you save a whole network round-trip and the 33% tax is trivial in absolute terms. It loses for anything big or shared, for two reasons. A data URI has no URL of its own, so it can't be cached: an inlined logo re-downloads inside every page that embeds it, instead of being fetched once. And a large base64 blob sits in the critical path — the browser has to parse past it before it can finish the stylesheet or the HTML.

function verdict(orig){
  if (orig <= 4 * 1024)  return ["good", "Great inline candidate — smaller than a round-trip."];
  if (orig <= 40 * 1024) return ["meh",  "Borderline — inline only if it's used on one page."];
  return ["bad", "Too big to inline — link it as a file so it can be cached."];
}
Enter fullscreen mode Exit fullscreen mode

One last thing people get wrong: a data URI is not private. Base64 looks scrambled but it's a public, reversible encoding — anyone can copy the string, run atob(), and get the exact original file back. Encode for convenience and request-saving, never for secrecy.

The lesson underneath the whole tool: a data URI is just a file that learned to travel as text, and text is 33% heavier than the bytes it carries.

Drop an image and see the real byte cost:
https://dev48v.infy.uk/solve/day42-image-to-datauri.html

Top comments (0)