Data URI vs Blob URL: Which Should You Use for Images?

2026-09-17 · 1122 words

When you have image bytes in memory and want to display them, there are two ways to get them into an img element without a server round trip. They are frequently treated as interchangeable, and they are not: one is a portable but expensive text encoding, the other is a cheap reference that only exists while the page does.

The short version

// data URI: the bytes are in the string
img.src = 'data:image/png;base64,' + base64Payload;

// blob URL: the bytes stay in a Blob, the URL is just a handle
const url = URL.createObjectURL(blob);
img.src = url;
// later, when the image is gone:
URL.revokeObjectURL(url);

What each one costs

The data URI's 33% is structural, not an implementation detail: Base64 packs three bytes into four characters, and image bytes are already compressed, so gzip does not win them back. A 300 KB photograph becomes a 400 KB string, and inside JavaScript that string is stored as UTF-16 — two bytes per character — so it occupies about 800 KB in memory, on top of the original bytes you still hold.

The blob URL's cost is essentially zero. The browser keeps the bytes once, in the Blob you already created, and the URL is a short string that points at them. There is no encode step, no decode step, and no second copy.

That difference decides most cases on its own. If the payload is large or transient — a preview of a file the user just selected, a generated image, a decoded Base64 payload — a blob URL is the right answer.

What each one can do

The trade-off runs the other way for portability, and this is where the choice stops being about size.

A data URI is a value. You can put it in a JSON response, write it into an HTML file, store it in localStorage, paste it into an email, embed it in CSS, or send it to a colleague in a chat message. It survives serialisation, page reloads and process restarts, because everything needed is in the string.

A blob URL is a handle. It is scoped to the document that created it: open the same URL in a new tab and it means nothing; save it to a file and it is a dead reference; reload the page and every blob URL the previous document created becomes invalid. It also cannot be used across origins.

JSON.stringify({ src: 'blob:https://example.com/5f2a...' });  // serialises fine
// ...and means nothing to whoever reads it

The memory leak nobody notices

Blob URLs must be revoked. Every createObjectURL call pins a Blob in memory until either the page is destroyed or you call revokeObjectURL. In a preview that updates on every keystroke — exactly the pattern this site's decoder uses — forgetting to revoke means you accumulate one pinned image per paste:

let currentUrl = null;

function show(blob) {
  if (currentUrl) URL.revokeObjectURL(currentUrl);   // release the previous one
  currentUrl = URL.createObjectURL(blob);
  img.src = currentUrl;
}

Revoking a URL that an <img> has already loaded is safe and correct: the image keeps its decoded pixels, and the underlying bytes are released. Revoking while an image is still loading cancels the load in some browsers, so revoke the old URL when you replace it, not the new one you just assigned.

The same leak exists for data URIs, but in a less visible form: a long string held in a DOM attribute is memory that only goes away when the element does. A 400 KB string set as img.src and then replaced leaves the browser free to release it, but the string in your own variable does not go anywhere until you drop the reference.

Where each one wins in practice

Use a blob URL when:

Use a data URI when:

Neither, when: you already have a real URL. An object or file served over HTTP is cached, streamable, and costs the browser nothing to display. Encoding bytes to Base64 to avoid a request is a trade that only pays off for small assets — see why Base64 makes your page 33% bigger for where that line falls.

There is one more case worth naming, because it is the one people reach for a data URI to solve: getting the string into a place that only accepts text — a JSON field, a config file, an email template. Neither a blob URL nor a data URI is the right answer there if the image is large; a real URL plus a text reference is. And if you are producing that string by hand rather than in code, Image to Base64 exists precisely to emit the right wrapper for the consumer you have in mind.

A note on decoding the other direction, since it comes up in the same code paths: if what you hold is a Base64 string rather than a Blob, you can go straight to a blob URL without touching atobawait (await fetch(dataUri)).blob() does the decoding internally and hands you something createObjectURL accepts. That is the shortest path from a pasted string to a rendered preview, and it is what this site's decoder does; Base64 to Image shows the result while also verifying that the bytes are what the header claimed.

The one security note worth carrying

Neither scheme executes scripts on its own: an <img> element does not run scripts inside an SVG, whether the source is a data URI or a blob URL. The risk appears when the resource stops being an image — navigating a top-level document to an image/svg+xml data URI, or passing user-supplied bytes to something that treats them as markup. Keep the rule narrow and concrete: images go in img, and a blob: URL is how you hand the browser bytes you already trust in memory.

One CSP detail catches people out in both directions. Data URIs need img-src data: to be allowed, and blob URLs need img-src blob:. A page with img-src 'self' blocks both — silently, with only a console violation to explain it. If you add either technique to a site with a strict CSP, add the matching source expression at the same time, or you will spend an afternoon debugging Base64 that was never broken.

Related reading

Try the Base64 to Image converter →