ArrayBuffer & typed arrays
Learn JavaScript typed arrays from bytes to ArrayBuffer, DataView, endian order, files, pixels, transfer, and resizable memory.
- 01Model bytes and buffersExplain bytes, hex notation, ArrayBuffer raw memory, and how several views can reinterpret the same bytes.
- 02Choose the right viewPick integer, float, BigInt, clamped, or DataView APIs based on range, rounding, endian order, and file layout.
- 03Move binary data safelyUse subarray, slice, set, resizable buffers, transfer, fetch/Blob arrayBuffer, and postMessage transfer lists deliberately.
Bytes before arrays
The previous lesson finished the Regular expressions module with regex performance. This first lesson in Binary data & memory switches from text patterns to bytes: file headers, pixels, audio samples, WebGL buffers, compressed data, and protocol packets.
A bit is a single 0 or 1. A byte is eight bits, so it can hold 256 different patterns. When we read a byte as an unsigned number, those patterns are 0 through 255. Hexadecimal is a compact way to write those patterns: 0xff means 255, 0x00 means 0, and 0x89 0x50 0x4e 0x47 is easier to scan than decimal bytes in a file signature.
const byte = 0b10101010;console.log(byte);console.log(byte.toString(16).padStart(2, "0"));console.log(byte.toString(2).padStart(8, "0"));An ArrayBuffer is a fixed-size or resizable block of raw memory. A typed array or DataView is a view that gives meaning to those bytes: unsigned byte, signed 16-bit integer, 32-bit float, 64-bit BigInt, and so on.
Normal arrays from Array basics can grow, hold mixed values, and store references. Typed arrays are dense and numeric. They are how JavaScript talks to browser and platform APIs that already think in bytes. The value rules build on Number & Math in depth and BigInt, but the storage is much closer to the metal.
ArrayBuffer and views over the same memory
STEP THROUGHYou cannot use buffer[0] to read an ArrayBuffer. It deliberately has no element type. Create a view, then the same bytes become meaningful. Multiple views can sit on the same buffer, and a write through one view is immediately visible through the others.
The shelf exists before anyone decides what is stored on it. A byte view labels every slot separately. A 16-bit view labels pairs. A DataView is a clipboard where you choose the offset and format for each read.
- In real life: A shelf with eight empty slots
- In JavaScript:
new ArrayBuffer(8) - In real life: A label that says each slot is one small box
- In JavaScript:
new Uint8Array(buffer) - In real life: A label that groups slots in pairs
- In JavaScript:
new Uint16Array(buffer) - In real life: A clipboard that reads any offset
- In JavaScript:
new DataView(buffer)
Where the analogy stops: Real memory is managed by the engine and may move internally. The shelf model is for understanding views, not physical addresses.
Step through one ArrayBuffer shared by several views. Writes through one view immediately change what the others see.
script
const bytes = new Uint8Array(buffer);bytes.set([0x34, 0x12, 0, 0]);const words = new Uint16Array(buffer);console.log(words[0].toString(16));words[1] = 0x7fff;console.log(Array.from(bytes.slice(2, 4)).join(" "));const view = new DataView(buffer);view.setInt8(4, -1);console.log(bytes[4]);console.log(ArrayBuffer.isView(view));The step-through uses Uint16Array to show shared memory, so the byte order for multi-byte typed-array elements follows the platform. Today that is almost always little-endian. When a file format or protocol says exactly which byte comes first, use DataView and pass the endian flag explicitly.
const buffer = new ArrayBuffer(8);const bytes = new Uint8Array(buffer);bytes.set([0x78, 0x56, 0x34, 0x12, 0xff, 0x00, 0x80, 0x3f]);const view = new DataView(buffer);console.log(view.getUint32(0, true).toString(16));console.log(view.getUint32(0, false).toString(16));console.log(new Float32Array(buffer, 4, 1)[0]);78 56 34 12 ff 00 80 3f0x123456780x7856341222136308061.0000303983688354Bytes 0–3 are 78 56 34 12. DataView reads them as little-endian 305419896 and big-endian 2018915346.
The typed array family: ranges, overflow and rounding
Every typed array constructor chooses three things: element size, value type, and conversion behavior. Integer typed arrays store a fixed number of bits. When a Number does not fit, ordinary integer typed arrays wrap modulo the element size. Floating arrays round to their precision. Uint8ClampedArray is the exception designed for pixels: it clamps into 0–255 instead of wrapping.
| Constructor | Bytes | Value kind | Range / precision | Common uses |
|---|---|---|---|---|
Int8Array | 1 | Signed integer | -128 to 127 | Small deltas, audio samples, protocol fields |
Uint8Array | 1 | Unsigned integer | 0 to 255 | Bytes from files, network packets, crypto, WASM memory |
Uint8ClampedArray | 1 | Unsigned clamped integer | 0 to 255, rounded and clamped | Canvas ImageData RGBA pixels |
Int16Array / Uint16Array | 2 | Signed / unsigned integer | -32768 to 32767 / 0 to 65535 | PCM audio, UTF-16-ish units, binary formats |
Int32Array / Uint32Array | 4 | Signed / unsigned integer | 32-bit ranges | Indexes, bit masks, WebGL/WebGPU data |
Float16Array | 2 | Half-precision float | About 3 decimal digits, finite max 65504 | GPU, ML, image and geometry data when supported |
Float32Array | 4 | Single-precision float | About 7 decimal digits | WebGL positions, audio buffers, binary scientific data |
Float64Array | 8 | Double-precision float | Same format as JavaScript number | High precision numeric arrays |
BigInt64Array / BigUint64Array | 8 | Signed / unsigned BigInt | 64-bit integer ranges | Large IDs, counters, file offsets, nanosecond timestamps |
- Read PNG width at byte offset 16.
- Edit canvas RGBA channels.
- Upload x/y/z positions to WebGL.
- Move a 10 MB buffer to a worker without copying.
- Scan every incoming packet byte.
- Store exact unsigned 64-bit IDs.
Sort each job by the first view or memory move you would reach for.
const signed = new Int8Array([127, 128, 129, -129]);const unsigned = new Uint8Array([255, 256, 257, -1]);const pixels = new Uint8ClampedArray([-20, 12.5, 13.5, 255.4, 300]);const floats = new Float32Array([Math.PI, 1 / 3, 1e40]);const bigs = new BigInt64Array([1n, -2n]);console.log(Array.from(signed).join(","));console.log(Array.from(unsigned).join(","));console.log(Array.from(pixels).join(","));console.log(Array.from(floats).join(","));console.log(Array.from(bigs).join(","));| Input | Printed output | Reason |
|---|---|---|
new Int8Array([127, 128, 129, -129]) | 127,-128,-127,127 | Integers wrap modulo 256, then read as signed two's-complement values. |
new Uint8Array([255, 256, 257, -1]) | 255,0,1,255 | Unsigned integers also wrap modulo 256. |
new Uint8ClampedArray([-20, 12.5, 13.5, 255.4, 300]) | 0,12,14,255,255 | Canvas pixels clamp to 0–255 and round to the nearest integer, with .5 ties to even. |
new Float32Array([Math.PI, 1 / 3, 1e40]) | 3.1415927410125732,0.3333333432674408,Infinity | Float32 rounds to 32-bit precision and overflows to Infinity. |
new BigInt64Array([1n, -2n]) | 1,-2 | BigInt typed arrays require BigInt values such as 1n; Number values throw a TypeError. |
BigInt64Array and BigUint64Array store 64-bit integer elements and require BigInt values such as 1n. Passing 1 as a Number throws a TypeError. Use them when the binary format really contains 64-bit integers and the value must stay exact.
Float16Array is the ES2025 half-precision float view. MDN currently marks it Baseline 2025: Chrome/Edge 135, Firefox 129, Safari 18.2, and Node.js 24+. It is not in Node 22, so production code should feature-detect before constructing it. MDN Baseline 2025: Chrome/Edge 135, Firefox 129, Safari 18.2, Node.js 24+. Node 22 does not have Float16Array.
const Float16 = globalThis.Float16Array;if (typeof Float16 === "function") { const values = new Float16([1, 1 / 3, 65504, 1e-8]); console.log(Array.from(values).join(","));} else { console.log("Float16Array not supported here");}DataView and endianness
BYTE ORDEREndianness answers: when a number uses several bytes, which byte comes first? Big-endian writes the most significant byte first. Little-endian writes the least significant byte first. CPU and typed-array native order is almost always little-endian today, but binary formats are not all the same.
DataView is the safest parser for structured binary data because every operation names a byte offset and, for multi-byte values, a little-endian boolean. Omit the boolean or pass false for big-endian. Pass true for little-endian.
Write one 32-bit value with DataView, then read those same four bytes both ways.
script
const view = new DataView(buffer);view.setUint32(0, 0x12345678, false);console.log(Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, "0")).join(" "));console.log(view.getUint32(0, false).toString(16));console.log(view.getUint32(0, true).toString(16));const buffer = new ArrayBuffer(4);const view = new DataView(buffer);view.setUint32(0, 0x12345678, false);console.log(Array.from(new Uint8Array(buffer)).map((byte) => byte.toString(16).padStart(2, "0")).join(" "));console.log(view.getUint32(0, false).toString(16));console.log(view.getUint32(0, true).toString(16));If the bytes come from your own in-memory numeric array, native typed arrays are convenient. If the bytes come from a file, network protocol, or documented binary format, parse with DataView so the byte order is visible in code review.
subarray, slice, set and ArrayBuffer.isView
Typed arrays look array-like, but the memory rules matter. subarray(start, end) creates a new view into the same buffer. slice(start, end) copies bytes into a new typed array. set(source, offset) bulk-copies values into an existing typed array, and ArrayBuffer.isView(value) tells you whether a value is a typed array or a DataView.
const bytes = new Uint8Array([1, 2, 3, 4]);const shared = bytes.subarray(1, 3);const copied = bytes.slice(1, 3);shared[0] = 99;console.log(bytes.join(","));console.log(copied.join(","));bytes.set([7, 8], 2);console.log(bytes.join(","));console.log(ArrayBuffer.isView(shared));console.log(ArrayBuffer.isView(bytes.buffer));| Operation | Copies bytes? | What changes when the source changes? |
|---|---|---|
view.subarray(1, 3) | No | The returned view sees later writes to the same bytes. |
view.slice(1, 3) | Yes | The returned typed array keeps its own copy. |
view.set([7, 8], 2) | Copies into existing storage | The target buffer changes at the destination offset. |
ArrayBuffer.isView(value) | No | Returns true for typed arrays and DataView, false for ArrayBuffer. |
Real files, pixels, WebGL and audio buffers
Binary APIs usually start by giving you an ArrayBuffer. Files and blobs use Blob.arrayBuffer(). fetch() responses have arrayBuffer(). Canvas ImageData stores pixels in a Uint8ClampedArray. WebGL, WebGPU, Web Audio, compression libraries, and WebAssembly all use typed arrays because they are dense, predictable, and cheap to hand to native code.
| Use case | How bytes arrive | Typed-array move |
|---|---|---|
| File headers | Read Blob.arrayBuffer() or File.slice(...).arrayBuffer(), then use DataView at documented offsets. | PNG stores width and height as big-endian 32-bit integers in the IHDR chunk. |
| Canvas pixels | ImageData.data is a Uint8ClampedArray of RGBA bytes. | Use the canvas lesson when you need to edit pixels or understand tainted canvases. |
| Networking and storage | fetch(url).then(r => r.arrayBuffer()), Cache API, IndexedDB, and streams all hand you byte chunks. | Keep a Uint8Array view for byte-wise parsing. |
| WebGL, WebGPU and audio | Graphics and audio APIs expect dense numeric buffers such as Float32Array and Uint16Array. | Typed arrays avoid per-element object overhead and copy cleanly to native code. |
PNG files start with an eight-byte signature. The IHDR chunk stores width at byte offset 16 and height at byte offset 20, both as big-endian unsigned 32-bit integers. That makes it a perfect DataView example.
const png = new Uint8Array([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x03, 0x20, 0x00, 0x00, 0x02, 0x58,]);const view = new DataView(png.buffer, png.byteOffset, png.byteLength);const signatureOk = png.slice(0, 8).join(" ") === "137 80 78 71 13 10 26 10";const width = view.getUint32(16, false);const height = view.getUint32(20, false);console.log(signatureOk);console.log(width + "x" + height);async function readPngSize(file) { const header = new Uint8Array(await file.slice(0, 24).arrayBuffer()); const view = new DataView(header.buffer, header.byteOffset, header.byteLength); const signature = header.slice(0, 8).join(" "); if (signature !== "137 80 78 71 13 10 26 10") throw new Error("Not a PNG"); return { width: view.getUint32(16, false), height: view.getUint32(20, false), };}SharedArrayBuffer lets multiple agents see the same bytes, and Atomics provides safe synchronization operations. That is a concurrency topic, not a beginner byte topic; connect it to Messaging & cloning and Web Workers when you need parallel work.
Resizable and transferable buffers
Modern ArrayBuffer can be created with maxByteLength. If supported, resize(newByteLength) changes the current size up to that maximum. Views with an explicit length are fixed-length; views created without a length can track the buffer as it grows and shrinks.
| Idea | Code shape | What it means |
|---|---|---|
| Resizable buffer | new ArrayBuffer(4, { maxByteLength: 8 }) | The buffer can grow or shrink up to the maximum, if the engine supports resizable ArrayBuffer. |
| Fixed-length view | new Uint8Array(buffer, 0, 4) | The view keeps its requested length while the range exists; if the buffer shrinks below it, it becomes out-of-bounds. |
| Length-tracking view | new Uint8Array(buffer) | When no length is supplied on a resizable buffer, the view tracks the current buffer length. |
| Transfer | buffer.transfer() | Moves bytes into a new buffer and detaches the old one, so the old buffer's byteLength becomes 0. |
| Fixed transfer | buffer.transferToFixedLength() | Moves bytes into a non-resizable destination. |
| Worker transfer list | worker.postMessage(buffer, [buffer]) | Transfers ownership across threads without copying; the sender's buffer becomes detached. |
const buffer = new ArrayBuffer(4, { maxByteLength: 8 });const fixed = new Uint8Array(buffer, 0, 4);const tracking = new Uint8Array(buffer);buffer.resize(6);console.log(fixed.length);console.log(tracking.length);buffer.resize(2);console.log(fixed.length);console.log(tracking.length);Transfer is about ownership, not sharing. buffer.transfer() moves the bytes into a new buffer and detaches the old one. transferToFixedLength() does the same but guarantees the destination is not resizable. A detached buffer reports byteLength as 0 and its old views can no longer read useful bytes.
const buffer = new ArrayBuffer(1024);const moved = buffer.transfer();console.log(buffer.byteLength);console.log(moved.byteLength);const fixed = moved.transferToFixedLength();console.log(fixed.resizable);worker.postMessage(buffer, [buffer]) transfers an ArrayBuffer to a worker without copying the bytes. That is different from structured cloning, and it is one reason the Messaging & cloning and Web Workers lessons matter for binary data.
Common misconceptions
| Misconception | Reality | Safer habit |
|---|---|---|
ArrayBuffer is an array | It is raw memory with byteLength; views provide indexing and element types. | Name variables buffer, bytes, view, or words to separate storage from interpretation. |
| All typed arrays choose the same byte order | Multi-byte typed arrays use native order; DataView lets you choose. | Use DataView for file and protocol parsers. |
subarray copies | subarray shares memory; slice copies. | Use slice before mutating if callers need isolation. |
| Out-of-range writes throw | Integer arrays wrap, clamped arrays clamp, floats round or overflow. | Test conversions with real output before parsing untrusted bytes. |
| Transferred means shared | Transferred means the sender is detached; shared memory is SharedArrayBuffer plus Atomics. | Pick transfer for ownership handoff, shared buffers for synchronized concurrency. |
Practice exercises
Predict both output lines before running the code.
const signed = new Int8Array([127, 128, 129, -129]);
const clamped = new Uint8ClampedArray([-1, 12.5, 13.5, 300]);
console.log(Array.from(signed).join(","));
console.log(Array.from(clamped).join(","));const signed = new Int8Array([127, 128, 129, -129]);
const clamped = new Uint8ClampedArray([-1, 12.5, 13.5, 300]);
console.log(Array.from(signed).join(","));
console.log(Array.from(clamped).join(","));The signed array prints 127,-128,-127,127. The clamped pixel array prints 0,12,14,255.
Write the two getUint32 reads a PNG size reader needs, then compare with the finished version.
const png = new Uint8Array([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x03, 0x20,
0x00, 0x00, 0x02, 0x58,
]);
const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
const signatureOk = png.slice(0, 8).join(" ") === "137 80 78 71 13 10 26 10";
const width = view.getUint32(16, false);
const height = view.getUint32(20, false);
console.log(signatureOk);
console.log(width + "x" + height);const png = new Uint8Array([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52,
0x00, 0x00, 0x03, 0x20,
0x00, 0x00, 0x02, 0x58,
]);
const view = new DataView(png.buffer, png.byteOffset, png.byteLength);
const signatureOk = png.slice(0, 8).join(" ") === "137 80 78 71 13 10 26 10";
const width = view.getUint32(16, false);
const height = view.getUint32(20, false);
console.log(signatureOk);
console.log(width + "x" + height);The signature is valid and the IHDR width and height decode to 800x600.
A teammate expected both views to be independent. Find the bug by predicting the original bytes and the copied bytes after the shared view changes.
const bytes = new Uint8Array([1, 2, 3, 4]);
const shared = bytes.subarray(1, 3);
const copy = bytes.slice(1, 3);
shared[0] = 99;
console.log(bytes.join(","));
console.log(copy.join(","));const bytes = new Uint8Array([1, 2, 3, 4]);
const shared = bytes.subarray(1, 3);
const copy = bytes.slice(1, 3);
shared[0] = 99;
console.log(bytes.join(","));
console.log(copy.join(","));Writing through shared mutates the original bytes, so the original prints 1,99,3,4; the copied slice remains 2,3.
Predict the two hexadecimal strings from the same four bytes.
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, 0x12345678, false);
console.log(view.getUint32(0, false).toString(16));
console.log(view.getUint32(0, true).toString(16));const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, 0x12345678, false);
console.log(view.getUint32(0, false).toString(16));
console.log(view.getUint32(0, true).toString(16));The big-endian read returns 12345678; the little-endian read returns 78563412.
Confirm that a 64-bit typed array stores a BigInt value exactly.
const ids = new BigUint64Array([2n ** 63n]);
console.log(ids[0] === 9223372036854775808n);const ids = new BigUint64Array([2n ** 63n]);
console.log(ids[0] === 9223372036854775808n);The stored value equals 9223372036854775808n, so the comparison prints true.
Check your understanding
Question 1 of 8What is an
ArrayBufferby itself?Choose an answer to see the explanation.
Question 2 of 8What does the signed 8-bit write print?
Read the code, then predictconsole.log(new Int8Array([127, 128, 129, -129]).join(','));Choose an answer to see the explanation.
Question 3 of 8What does the clamped pixel array print?
Read the code, then predictconsole.log(new Uint8ClampedArray([-1, 12.5, 13.5, 300]).join(','));Choose an answer to see the explanation.
Question 4 of 8Which value belongs in a
BigInt64Arrayconstructor?Choose an answer to see the explanation.
Question 5 of 8What does reading the same bytes as little-endian print?
Read the code, then predictconst b = new ArrayBuffer(4); const v = new DataView(b); v.setUint32(0, 0x12345678, false); console.log(v.getUint32(0, true).toString(16));Choose an answer to see the explanation.
Question 6 of 8Which operation shares memory with the original typed array?
Choose an answer to see the explanation.
Question 7 of 8What happens to the sender's buffer after a successful transfer?
Choose an answer to see the explanation.
Question 8 of 8What is the current practical status of
Float16Array?Choose an answer to see the explanation.
Key takeaways
- A byte is eight bits: an unsigned byte reads as 0–255, often written with two hex digits.
ArrayBufferstores raw memory; typed arrays andDataViewinterpret it.- Integer typed arrays wrap,
Uint8ClampedArrayclamps and rounds for pixels, floats round to their precision, and BigInt arrays require BigInt values. - Use
DataViewfor explicit offsets and endian order in binary formats. subarrayshares,slicecopies,setbulk-copies, andArrayBuffer.isViewdetects typed-array/DataView views.- Resizable buffers, transfer, worker transfer lists, and SharedArrayBuffer are about ownership and memory lifetime as much as bytes.
Up next: Text encoding & Base64, where these same bytes become strings and encoded text.