Area: Binary data in the browser
Related: Browser APIs (map), IndexedDB, Web Workers, Clipboard and DataTransfer, Sandboxed iframes

What is it?

A Blob is a chunk of raw bytes with a MIME type, like “this is 2 MB of image/png”. A File is a Blob with a name and a last-modified date. An object URL (blob:https://example.com/3f2a...) is a temporary URL that points at a Blob in memory, so you can use it anywhere a URL works: <img src>, <a href>, <video src>, an iframe.

const blob = new Blob(["# Hello"], { type: "text/markdown" });
const url = URL.createObjectURL(blob); // "blob:https://example.com/9b1c..."
// use it...
URL.revokeObjectURL(url); // free it

A Blob is the bytes. An object URL is a short-lived address for those bytes, valid only inside this page.

Where Blobs come from

  • new Blob([parts], { type }), where parts can be strings, ArrayBuffers, typed arrays or other Blobs
  • await response.blob() from fetch
  • <input type="file"> and drag-and-drop give you File objects
  • canvas.toBlob(callback, "image/png")
  • navigator.clipboard.read() and paste events
  • IndexedDB, which can store and return Blobs directly
  • MediaRecorder for recorded audio and video

Working with a Blob

Blobs are immutable. You don’t edit one, you make a new one.

blob.size;            // bytes
blob.type;            // "image/png"
blob.slice(0, 1024);  // a new Blob for the first 1 KB, no copying
await blob.text();         // read as a string
await blob.arrayBuffer();  // read as bytes
blob.stream();             // a ReadableStream, for huge files

slice is cheap. It doesn’t copy, it just points at part of the original. That’s how big uploads get split into chunks: slice the File into 5 MB pieces and upload each.

The old FileReader API (readAsText, readAsDataURL) still works, but blob.text() and blob.arrayBuffer() are simpler and return promises.

Where the bytes live

A Blob may not be in your JavaScript memory at all. The browser can keep a large Blob on disk, or in the browser process, and give you a handle. That’s why a File from a 2 GB video upload doesn’t freeze the page. Only when you call arrayBuffer() does it all get pulled into memory, so for big files use stream() or slice().

Object URLs in detail

URL.createObjectURL(blob) registers the blob in a table owned by the document (or worker) and returns a blob: URL for it.

  • The URL works only in this page (same document). Pasting it into another tab won’t work, and it’s dead after the page closes.
  • As long as the URL is registered, the Blob can’t be garbage collected. If you create URLs in a loop and never revoke them, you leak memory. On an image-heavy page this adds up to hundreds of MB.
  • URL.revokeObjectURL(url) removes the entry. Anything already loaded from it, like an image already shown, keeps working. New loads of the URL fail.

When to revoke

  • Image or video on screen: revoke when the element is removed or the source changes (in React, in the effect cleanup).
  • Download link: revoke after the click has started the download. A short setTimeout after click() is the usual pattern, since some browsers need the URL to exist for a moment.
  • Cache of URLs: if you keep a Map from id to object URL to reuse, revoke when you evict entries, and all of them on sign-out.
function BlobImage({ blob }: { blob: Blob }) {
  const [src, setSrc] = useState<string>();
  useEffect(() => {
    const url = URL.createObjectURL(blob);
    setSrc(url);
    return () => URL.revokeObjectURL(url);
  }, [blob]);
  return src ? <img src={src} alt="" /> : null;
}

Object URLs vs data URLs

A data URL holds the whole file inside the URL string: data:image/png;base64,iVBORw0KG....

blob: URLdata: URL
Sizeshort pointerthe whole file, base64 (about 33% bigger than the bytes)
Memoryone copy of the bytesa giant JS string, plus decoding
Lifetimeuntil revoked or the page closesas long as the string exists
Can be saved in a document or databaseno, it’s page-localyes, it’s self-contained
Works across tabs or after reloadnoyes

Use blob URLs for showing and downloading things at runtime. Use data URLs only for small things that need to be self-contained, like a tiny inline icon, or an image that must survive being copied into another document.

Never store a blob: URL in a saved document. After a reload it points at nothing, and users see broken images.

Pattern 1: download a file you generated

function download(content: string, filename: string, type = "text/plain") {
  const blob = new Blob([content], { type });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = filename; // makes it download instead of navigating
  document.body.appendChild(a);
  a.click();
  a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

download only works for same-origin and blob:/data: URLs. For a cross-origin file URL, the browser ignores it and just navigates.

In sandboxed iframes without allow-downloads (for example some embedded app hosts), this is blocked entirely. Then you need another route, like opening a link in the system browser.

Pattern 2: show an image that needs an auth header

An <img src="https://api.example.com/images/42"> can’t send an Authorization: Bearer ... header. If the image is private and the server needs a token, the <img> gets a 401.

The fix:

async function loadPrivateImage(url: string, token: string): Promise<string> {
  const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return URL.createObjectURL(await response.blob());
}
 
img.src = await loadPrivateImage("/api/images/42", token);

Then cache the object URLs by image id so you don’t fetch the same image twice, and revoke them when they’re evicted.

Pattern 3: preview a file before uploading

input.onchange = () => {
  const file = input.files![0];
  preview.src = URL.createObjectURL(file); // instant, nothing uploaded yet
};

Much faster than reading it into a data URL, especially for big photos.

Content Security Policy

If your site has a CSP, blob: URLs must be allowed in the right directive, or they’re blocked:

img-src 'self' blob: data:;
media-src 'self' blob:;
worker-src 'self' blob:;

Be careful with script-src blob:. It lets any script that can create a Blob run code from it, which weakens the policy.

A real-world example

In a notes app where images and attachments are private and need an access token:

  • Private images. The <img> tag can’t send the token, so the app fetches each image with the token, turns the response into a Blob, and shows it through an object URL. It keeps a small cache of those URLs and revokes them when they’re evicted. This is pattern 2.
  • Profile photos go the other way. The API returns a photo as a Blob. The app loads it through an object URL just long enough to crop and shrink it on a canvas, then keeps the result as a tiny WebP data URL. Avatars are small and reused all over the app, which is exactly the case where a self-contained data URL beats a page-local blob URL. Only data:image/...;base64, values are allowed to render as avatars, so nothing remote sneaks in.
  • Exports and downloads. Exports, image downloads, old versions and transcripts are turned into Blobs and downloaded with the link trick. Inside a host that blocks downloads in its iframe, the app opens the file in the system browser instead.
  • File previews. Image files are previewed straight from a Blob.

Gotchas

  • Every createObjectURL needs a revokeObjectURL, or the Blob leaks.
  • blob: URLs only live inside the current page. Never save them.
  • Data URLs are about a third bigger and live as big strings. Use them for small, self-contained things only.
  • arrayBuffer() on a huge File loads it all into memory. Use stream() or slice().
  • <img> can’t send auth headers. Fetch, Blob, object URL.
  • a.download is ignored for cross-origin URLs.
  • CSP must allow blob: in img-src/media-src.

Practice

  1. Generate a CSV from an array of objects and download it with the link trick.
  2. Build an image preview for a file input. Then open DevTools’ Memory panel, pick 50 images without revoking, and watch memory climb. Add revoking and compare.
  3. Serve an image that requires an Authorization header from a small local server. Show it with the fetch + object URL pattern.
  4. Upload a large file in 5 MB slices and show progress.

Sources worth reading

  • MDN: Blob, File, URL.createObjectURL(), Using files from web applications
  • MDN: data: URLs
  • MDN: CSP: img-src