Blob

ArrayBuffer and its views live in the ECMAScript standard. They’re part of the language itself, available anywhere JavaScript runs.

Blob is different. It comes from the browser, defined by the File API specification. Where an ArrayBuffer is raw bytes with no label, a Blob is raw bytes plus a label describing what kind of data they are.

That label is a type — usually a MIME-type like image/png or text/plain. The bytes themselves come from blobParts: a sequence of other Blob objects, strings, and BufferSource values (ArrayBuffer or a typed array). The browser flattens all of those parts into one continuous chunk of binary data.

Blob
blobParts
“string”
Uint8Array
Blob
type
“image/png”
A Blob wraps a sequence of parts and tags the whole thing with a MIME-type.

The constructor takes those two things:

new Blob(blobParts, options);
  • blobParts — an array of Blob / BufferSource / String values.
  • options — an optional object:
    • type — the Blob type, usually a MIME-type, e.g. image/png.
    • endings — whether to rewrite line endings to match the current OS (\r\n on Windows, \n elsewhere). The default is "transparent" (leave the bytes untouched); "native" performs the rewrite.

A blob from a single string:

// create a Blob from a string
let blob = new Blob(["<p>Saved page</p>"], {type: 'text/html'});
// note: the first argument must be an array [...]

That array requirement catches people constantly. new Blob("hello") does not do what you want — a string is iterable, so the browser would treat each character as a separate part. Always pass ["hello"].

You can mix part types freely in the same array:

// create a Blob from a typed array and strings
let pixel = new Uint8Array([80, 105, 120, 101, 108]); // "Pixel" in binary form

let blob = new Blob([pixel, ' ', 'data'], {type: 'text/plain'});

Here the five bytes 80, 105, 120, 101, 108 are the ASCII codes for P, i, x, e, l. The browser concatenates the typed array, the space string, and the 'data' string into one blob whose contents read Pixel data.

Slicing a blob

You can pull a piece out of a blob with slice:

blob.slice([byteStart], [byteEnd], [contentType]);
  • byteStart — the first byte to include, default 0.
  • byteEnd — one past the last byte to include (exclusive), default the end of the blob.
  • contentType — the type of the new blob, default the same as the source.

The arguments mirror array.slice, and negative numbers count from the end just like there.

P
0
i
1
x
2
e
3
l
4
5
blob.slice(2, 5) returns a new blob covering bytes 2, 3 and 4 — byteEnd is exclusive.

Blob as URL

A blob can act as a URL for <a>, <img>, or any other tag that expects one, so the browser can display or link to its contents.

Because a blob carries a type, it also works cleanly for downloads and uploads — the type becomes the Content-Type header in network requests.

Start with a download link. Clicking it saves a blob generated on the fly, with Report ready. inside, as a file:

<!-- the download attribute forces a save instead of navigation -->
<a download="report.txt" href='#' id="link">Download</a>

<script>
let blob = new Blob(["Report ready."], {type: 'text/plain'});

link.href = URL.createObjectURL(blob);
</script>

You can also skip the HTML entirely: build the link in JavaScript and trigger a click with link.click(). The download starts on its own:

let link = document.createElement('a');
link.download = 'report.txt';

let blob = new Blob(['Report ready.'], {type: 'text/plain'});

link.href = URL.createObjectURL(blob);

link.click();

URL.revokeObjectURL(link.href);

URL.createObjectURL takes a Blob and hands back a unique URL of the form blob:<origin>/<uuid>.

link.href ends up looking like this:

blob:https://example.com/cdf8fd21-15c9-4015-b8e8-d5bac0ddf982

For every URL it creates this way, the browser keeps an internal URL → Blob mapping. That’s why the URL can be so short while still pointing at arbitrarily large binary data — the actual bytes live in the browser’s memory, and the URL is just a key into that table.

browser memory
Blob { 13 bytes, text/plain }
An object URL is a short key; the real bytes stay in the browser's internal map.

Such a URL (and any link using it) only works inside the current document while it stays open. Within that document it can reference the blob anywhere a URL is expected — <img>, <a>, and so on.

There’s a cost. As long as the mapping exists, the browser has to keep the blob in memory. It can’t be freed.

The mapping is cleared automatically when the document unloads, so the blob is released then. For a long-lived single-page app, though, “when the document unloads” might be hours away, or never during a session.

So creating an object URL pins that blob in memory, even after you’re done with it.

URL.revokeObjectURL(url) deletes the entry from the internal mapping. Once nothing else references the blob, it becomes eligible for garbage collection and the memory is reclaimed.

In the click-and-download example above, the blob only needs to exist long enough for that one download, so URL.revokeObjectURL(link.href) runs immediately after the click.

In the earlier HTML-link example, revoking would be a mistake. Removing the mapping makes the URL dead, so the link would stop working. There we leave the mapping in place and let the document unload clean it up.

Blob to base64

Instead of URL.createObjectURL, you can turn a blob into a base64-encoded string.

Base64 represents binary data using a small set of safe, printable ASCII characters. Every three bytes of input become four characters drawn from A–Z, a–z, 0–9, +, and /. The payoff: base64 slots into a “data URL”, which you can use anywhere a normal URL goes.

A data URL has the shape data:[<mediatype>][;base64],<data>. The whole resource is inlined into the string itself — no separate network request, no mapping to clean up.

Here’s a tiny image encoded that way:

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAASUlEQVR42mOosXoLQW9f/sCFGOCK8KhDUYRLHQMQE1THAKHwq2OAs/CoY0Dm4FLHgGYyVnUMmM7EVMeA1c9o6hhwBSBRipDVAQCB9VTeCs4OmwAAAABJRU5ErkJggg==">

The browser decodes the string and renders the picture: <img src=“data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAASUlEQVR42mOosXoLQW9f/sCFGOCK8KhDUYRLHQMQE1THAKHwq2OAs/CoY0Dm4FLHgGYyVnUMmM7EVMeA1c9o6hhwBSBRipDVAQCB9VTeCs4OmwAAAABJRU5ErkJggg==”>

To convert a Blob into base64, use the built-in FileReader. It reads blob data in several formats, and one of them is a data URL. The next chapter covers FileReader in more depth.

Here’s the download demo again, now routed through base64:

let link = document.createElement('a');
link.download = 'report.txt';

let blob = new Blob(['Report ready.'], {type: 'text/plain'});

let reader = new FileReader();
reader.readAsDataURL(blob); // convert the blob to base64 and call onload

reader.onload = function() {
  link.href = reader.result; // data url
  link.click();
};

readAsDataURL is asynchronous, which is why the click waits inside the onload handler — reader.result isn’t ready until the read finishes.

Both approaches produce a usable URL from a blob. Usually URL.createObjectURL(blob) is the simpler, faster choice.

Image to blob

You can make a blob out of an image, a cropped piece of one, or even a rendered screenshot of the page. That’s convenient when you want to upload the result somewhere.

Image work goes through the <canvas> element:

  1. Draw an image (or part of it) onto a canvas with canvas.drawImage.
  2. Call canvas.toBlob(callback, format, quality), which builds a Blob and passes it to callback when the encoding finishes.

The example below just copies the image straight across, but the same canvas step is where you’d crop, rotate, or otherwise transform the pixels before encoding:

// take any image
let img = document.querySelector('img');

// make a <canvas> of the same size
let canvas = document.createElement('canvas');
canvas.width = img.clientWidth;
canvas.height = img.clientHeight;

let context = canvas.getContext('2d');

// copy the image onto it (drawImage can also crop)
context.drawImage(img, 0, 0);
// we could context.rotate(), and do many other things on canvas

// toBlob is async — the callback runs when it's done
canvas.toBlob(function(blob) {
  // blob ready, download it
  let link = document.createElement('a');
  link.download = 'example.png';

  link.href = URL.createObjectURL(blob);
  link.click();

  // drop the internal reference so the browser can free the memory
  URL.revokeObjectURL(link.href);
}, 'image/png');
<img>
— drawImage →
<canvas>
— toBlob →
Blob {image/png}
The image-to-blob pipeline: pixels in a canvas become an encoded, typed Blob.

If you’d rather use async/await than a callback, wrap toBlob in a promise:

let blob = await new Promise(resolve => canvasElem.toBlob(resolve, 'image/png'));

For a full-page screenshot, a library like html2canvas does the heavy lifting. It walks the DOM and paints it onto a <canvas>. From there you get a blob exactly as shown above.

From Blob to ArrayBuffer

The Blob constructor accepts almost anything as input, including any BufferSource. Going the other direction — from a blob down to raw bytes — is just as easy when you need low-level processing.

blob.arrayBuffer() returns a promise that resolves to an ArrayBuffer:

// get an ArrayBuffer from a blob
const bufferPromise = await blob.arrayBuffer();

// or, with .then
blob.arrayBuffer().then(buffer => /* process the ArrayBuffer */);

Once you have the ArrayBuffer, wrap a view over it — a Uint8Array, DataView, or similar — and read or write individual bytes as needed.

From Blob to stream

Reading a blob into an ArrayBuffer pulls the entire thing into memory at once. For a blob larger than about 2 GB that gets expensive fast, and on some platforms it may not fit at all. The alternative is to read the blob as a stream.

A stream is an object you consume piece by piece instead of all at once. Streams are a topic of their own — see the Streams API — but the idea is simple: pull one chunk, handle it, release it, repeat. Memory stays flat no matter how large the source is.

blob.stream() returns a ReadableStream. Reading from it yields the blob’s data one fragment at a time:

// get a ReadableStream from the blob
const readableStream = blob.stream();
const stream = readableStream.getReader();

while (true) {
  // each iteration: value is the next blob fragment
  let { done, value } = await stream.read();
  if (done) {
    // no more data in the stream
    console.log('all blob processed.');
    break;
  }

  // do something with the chunk we just read
  console.log(value);
}
arrayBuffer()
whole blob in memory
stream()
chunk 1
chunk 2
chunk 3
→ done
ArrayBuffer loads everything at once; a stream delivers fixed-size chunks over time.

Summary

ArrayBuffer, Uint8Array, and the other BufferSource types are “binary data”. A Blob is “binary data with a type” — the same bytes, tagged with a MIME-type.

That tag is what makes blobs the natural currency for uploads and downloads, which are everywhere in browser code. Request APIs like XMLHttpRequest and fetch accept a Blob directly, right alongside the other binary types.

Converting between a blob and the low-level types is straightforward in both directions:

  • Build a Blob from a typed array (or strings, or other blobs) with the new Blob(...) constructor.
  • Get an ArrayBuffer back with blob.arrayBuffer(), then lay a view over it for byte-level work.

When a blob is large enough that loading it whole would strain memory, read it as a stream instead. blob.stream() returns a ReadableStream that hands you the contents chunk by chunk, keeping memory usage flat.