JavaScript — TypedArrays & Buffers
Typed arrays provide a mechanism for reading and writing raw binary data in JavaScript. Unlike regular JavaScript arrays, typed arrays operate on a fixed-size ArrayBuffer — a contiguous block of memory — and interpret each byte as a specific numeric type (int8, uint16, float32, etc.).
They were introduced to enable WebGL, audio processing, WebSockets, file APIs, and other performance-critical binary protocols. Typed arrays bridge the gap between JavaScript and low-level systems programming, giving developers direct memory access with predictable performance characteristics.
This page covers ArrayBuffer, all typed array types, DataView for heterogeneous data, and practical use cases for binary data manipulation.
| 1 | // ArrayBuffer — raw binary memory |
| 2 | const buffer = new ArrayBuffer(16); // 16 bytes |
| 3 | console.log(buffer.byteLength); // 16 |
| 4 | |
| 5 | // TypedArray — a view into the buffer |
| 6 | const view = new Uint8Array(buffer); |
| 7 | view[0] = 42; |
| 8 | view[1] = 255; |
| 9 | |
| 10 | console.log(view[0]); // 42 |
| 11 | console.log(view[1]); // 255 |
| 12 | console.log(view.length); // 16 (elements) |
| 13 | |
| 14 | // Different typed array types interpret the same bytes differently |
| 15 | const floatView = new Float32Array(buffer); |
| 16 | console.log(floatView[0]); // 2.121995791e-314 (bytes interpreted as float) |
| 17 | |
| 18 | // DataView — read/write with explicit type and endianness |
| 19 | const dataView = new DataView(buffer); |
| 20 | dataView.setInt32(0, 12345, true); // little-endian |
| 21 | console.log(dataView.getInt32(0, true)); // 12345 |
ArrayBuffer is a fixed-length sequence of raw bytes. You cannot read or write directly to an ArrayBuffer — you must create a view (typed array or DataView) that interprets the bytes as specific types. ArrayBuffers can be transferred between contexts (workers) without copying via the Transferable interface.
| Method | Description |
|---|---|
| new ArrayBuffer(length) | Creates a buffer of given byte length, initialized to 0 |
| buffer.byteLength | Returns the length of the buffer in bytes |
| buffer.slice(start, end) | Returns a new buffer containing bytes from start to end |
| ArrayBuffer.isView(value) | Returns true if value is a TypedArray or DataView |
| buffer.transfer() | Transfers ownership to a new buffer (detaches original) |
| 1 | // Creating and manipulating ArrayBuffers |
| 2 | const buffer = new ArrayBuffer(8); |
| 3 | console.log(buffer.byteLength); // 8 |
| 4 | |
| 5 | // slice — create a view of a portion |
| 6 | const slice = buffer.slice(2, 6); |
| 7 | console.log(slice.byteLength); // 4 |
| 8 | |
| 9 | // Transferable — zero-copy ownership transfer |
| 10 | // Useful for transferring between workers without copying |
| 11 | const transferable = new ArrayBuffer(1024); |
| 12 | const transferred = transferable.transfer(); |
| 13 | console.log(transferable.byteLength); // 0 (detached!) |
| 14 | console.log(transferred.byteLength); // 1024 |
| 15 | |
| 16 | // SharedArrayBuffer — for shared memory concurrency |
| 17 | // Requires cross-origin isolation headers |
| 18 | const shared = new SharedArrayBuffer(1024); |
| 19 | const sharedView = new Uint8Array(shared); |
| 20 | // Can be accessed from multiple workers simultaneously |
| 21 | // Use Atomics for synchronization |
| 22 | |
| 23 | // Resizable ArrayBuffers (ES2024+) |
| 24 | const resizable = new ArrayBuffer(16, { maxByteLength: 1024 }); |
| 25 | console.log(resizable.resizable); // true |
| 26 | resizable.resize(32); |
| 27 | console.log(resizable.byteLength); // 32 |
| 28 | |
| 29 | // Detached buffers |
| 30 | const detachable = new ArrayBuffer(64); |
| 31 | const buf2 = detachable.transfer(); |
| 32 | console.log(detachable.detached); // true |
| 33 | // Any access to detachable throws TypeError |
info
JavaScript provides nine typed array constructors, each corresponding to a specific binary numeric format. All typed arrays share the same API — they differ only in element type, size, and range. Choosing the right type affects memory usage, precision, and performance.
| Type | Element Size | Range | Use Case |
|---|---|---|---|
| Int8Array | 1 byte | -128 to 127 | Small signed integers |
| Uint8Array | 1 byte | 0 to 255 | Bytes, raw data |
| Uint8ClampedArray | 1 byte | 0 to 255 (clamped) | Canvas pixel data |
| Int16Array | 2 bytes | -32768 to 32767 | Audio samples |
| Uint16Array | 2 bytes | 0 to 65535 | Unicode, pixel channels |
| Int32Array | 4 bytes | -2^31 to 2^31-1 | Standard integers |
| Uint32Array | 4 bytes | 0 to 2^32-1 | Unsigned ints, flags |
| Float32Array | 4 bytes | ~1.2e-38 to 3.4e38 | Audio, WebGL, sensors |
| Float64Array | 8 bytes | ~5e-324 to 1.8e308 | High precision, scientific |
| BigInt64Array | 8 bytes | -2^63 to 2^63-1 | Large integers |
| BigUint64Array | 8 bytes | 0 to 2^64-1 | Large unsigned ints |
| 1 | // All typed arrays share the same API |
| 2 | const int8 = new Int8Array(4); |
| 3 | int8[0] = 127; // max positive |
| 4 | int8[1] = -128; // min negative |
| 5 | int8[2] = 200; // wraps to -56 (overflow!) |
| 6 | console.log(int8); // Int8Array [ 127, -128, -56, 0 ] |
| 7 | |
| 8 | // Uint8ClampedArray — clamps instead of wrapping |
| 9 | const clamped = new Uint8ClampedArray(4); |
| 10 | clamped[0] = -1; // clamps to 0 |
| 11 | clamped[1] = 999; // clamps to 255 |
| 12 | clamped[2] = 128; |
| 13 | console.log(clamped); // Uint8ClampedArray [ 0, 255, 128, 0 ] |
| 14 | |
| 15 | // Float32 vs Float64 precision |
| 16 | const f32 = new Float32Array(1); |
| 17 | const f64 = new Float64Array(1); |
| 18 | f32[0] = 0.1; |
| 19 | f64[0] = 0.1; |
| 20 | console.log(f32[0]); // 0.10000000149011612 (less precise) |
| 21 | console.log(f64[0]); // 0.1 (more precise) |
| 22 | |
| 23 | // BigInt typed arrays |
| 24 | const big = new BigInt64Array(2); |
| 25 | big[0] = 9007199254740993n; |
| 26 | big[1] = BigInt(Number.MAX_SAFE_INTEGER) + 2n; |
| 27 | console.log(big[0]); // 9007199254740993n |
| 28 | console.log(big[1]); // 9007199254740993n |
| 29 | |
| 30 | // They all share the same prototype methods |
| 31 | console.log(Int8Array.prototype.__proto__ === TypedArray.prototype); |
| 32 | // true |
| 33 | |
| 34 | // .from() and .of() static methods |
| 35 | const fromArray = Uint8Array.from([1, 2, 3]); |
| 36 | const fromOf = Uint16Array.of(10, 20, 30); |
best practice
DataView provides a low-level API for reading and writing multiple numeric types from an ArrayBuffer with explicit control over byte offset and endianness. Unlike typed arrays — which enforce a single element type across the entire buffer — DataView handles heterogeneous binary data.
| 1 | // DataView — flexible binary data access |
| 2 | const buffer = new ArrayBuffer(16); |
| 3 | const dv = new DataView(buffer); |
| 4 | |
| 5 | // Writing different types at different offsets |
| 6 | dv.setInt8(0, -42); |
| 7 | dv.setUint16(1, 30000, true); // little-endian |
| 8 | dv.setFloat32(3, 3.14159, true); |
| 9 | dv.setUint32(7, 0xDEADBEEF, false); // big-endian |
| 10 | |
| 11 | // Reading back |
| 12 | console.log(dv.getInt8(0)); // -42 |
| 13 | console.log(dv.getUint16(1, true)); // 30000 |
| 14 | console.log(dv.getFloat32(3, true)); // 3.141590118408203 |
| 15 | console.log(dv.getUint32(7, false)); // 3735928495 (0xDEADBEEF) |
| 16 | |
| 17 | // DataView properties |
| 18 | console.log(dv.buffer === buffer); // true |
| 19 | console.log(dv.byteLength); // 16 |
| 20 | console.log(dv.byteOffset); // 0 |
| 21 | |
| 22 | // DataView from a specific offset in a larger buffer |
| 23 | const subBuffer = buffer.slice(4, 12); |
| 24 | const subView = new DataView(subBuffer); |
| 25 | console.log(subView.byteOffset); // 0 |
| 26 | console.log(subView.byteLength); // 8 |
| 27 | |
| 28 | // Endianness matters |
| 29 | const testBuf = new ArrayBuffer(4); |
| 30 | const testView = new DataView(testBuf); |
| 31 | testView.setUint32(0, 0x01020304, true); // little-endian |
| 32 | console.log(testView.getUint8(0)); // 4 (LSB first) |
| 33 | console.log(testView.getUint8(3)); // 1 (MSB last) |
| 34 | |
| 35 | testView.setUint32(0, 0x01020304, false); // big-endian |
| 36 | console.log(testView.getUint8(0)); // 1 (MSB first) |
| 37 | console.log(testView.getUint8(3)); // 4 (LSB last) |
info
Typed arrays excel at high-performance binary data processing. Common operations include encoding/decoding binary protocols, processing pixel data, audio waveform manipulation, and serialization. These operations benefit from the predictable memory layout and lack of boxing overhead.
String Encoding & Decoding
| 1 | // Convert strings to/from typed arrays |
| 2 | function stringToBytes(str) { |
| 3 | // TextEncoder produces Uint8Array |
| 4 | return new TextEncoder().encode(str); |
| 5 | } |
| 6 | |
| 7 | function bytesToString(bytes) { |
| 8 | return new TextDecoder().decode(bytes); |
| 9 | } |
| 10 | |
| 11 | const utf8bytes = stringToBytes("Hello, 世界"); |
| 12 | console.log(utf8bytes); // Uint8Array [72, 101, 108, ...] |
| 13 | |
| 14 | const back = bytesToString(utf8bytes); |
| 15 | console.log(back); // "Hello, 世界" |
| 16 | |
| 17 | // Manual UTF-8 encoding for low-level control |
| 18 | function encodeUTF8(str) { |
| 19 | const buf = new ArrayBuffer(str.length * 4); // worst case |
| 20 | const view = new DataView(buf); |
| 21 | let offset = 0; |
| 22 | |
| 23 | for (let i = 0; i < str.length; i++) { |
| 24 | let code = str.charCodeAt(i); |
| 25 | |
| 26 | if (code < 0x80) { |
| 27 | view.setUint8(offset++, code); |
| 28 | } else if (code < 0x800) { |
| 29 | view.setUint8(offset++, 0xC0 | (code >> 6)); |
| 30 | view.setUint8(offset++, 0x80 | (code & 0x3F)); |
| 31 | } else if (code < 0xD800 || code >= 0xE000) { |
| 32 | view.setUint8(offset++, 0xE0 | (code >> 12)); |
| 33 | view.setUint8(offset++, 0x80 | ((code >> 6) & 0x3F)); |
| 34 | view.setUint8(offset++, 0x80 | (code & 0x3F)); |
| 35 | } else { |
| 36 | // Surrogate pair |
| 37 | const codePoint = str.codePointAt(i); |
| 38 | view.setUint8(offset++, 0xF0 | (codePoint >> 18)); |
| 39 | view.setUint8(offset++, 0x80 | ((codePoint >> 12) & 0x3F)); |
| 40 | view.setUint8(offset++, 0x80 | ((codePoint >> 6) & 0x3F)); |
| 41 | view.setUint8(offset++, 0x80 | (codePoint & 0x3F)); |
| 42 | i++; |
| 43 | } |
| 44 | } |
| 45 | |
| 46 | return new Uint8Array(buf.slice(0, offset)); |
| 47 | } |
Binary Protocol Parsing
| 1 | // Parsing a binary protocol header (e.g., TCP, file format) |
| 2 | class BinaryHeader { |
| 3 | static from(buffer) { |
| 4 | const dv = new DataView(buffer); |
| 5 | |
| 6 | return { |
| 7 | version: dv.getUint8(0), |
| 8 | flags: dv.getUint8(1), |
| 9 | length: dv.getUint32(2, false), // big-endian |
| 10 | checksum: dv.getUint16(6, false), // big-endian |
| 11 | timestamp: dv.getFloat64(8, true), // little-endian |
| 12 | reserved: new Uint8Array(buffer, 16, 8) |
| 13 | }; |
| 14 | } |
| 15 | |
| 16 | static create(version, flags, length, timestamp) { |
| 17 | const buffer = new ArrayBuffer(24); |
| 18 | const dv = new DataView(buffer); |
| 19 | |
| 20 | dv.setUint8(0, version); |
| 21 | dv.setUint8(1, flags); |
| 22 | dv.setUint32(2, length, false); |
| 23 | dv.setUint16(6, 0, false); // checksum placeholder |
| 24 | dv.setFloat64(8, timestamp, true); |
| 25 | new Uint8Array(buffer, 16, 8).fill(0); // reserved |
| 26 | |
| 27 | return buffer; |
| 28 | } |
| 29 | } |
| 30 | |
| 31 | const header = BinaryHeader.create(1, 0x03, 1024, Date.now()); |
| 32 | const parsed = BinaryHeader.from(header); |
| 33 | console.log(parsed.version); // 1 |
| 34 | console.log(parsed.flags); // 3 |
| 35 | console.log(parsed.length); // 1024 |
Image Pixel Manipulation
| 1 | // Canvas pixel data manipulation with Uint8ClampedArray |
| 2 | function grayscale(imageData) { |
| 3 | const pixels = imageData.data; // Uint8ClampedArray |
| 4 | for (let i = 0; i < pixels.length; i += 4) { |
| 5 | const r = pixels[i]; |
| 6 | const g = pixels[i + 1]; |
| 7 | const b = pixels[i + 2]; |
| 8 | const gray = 0.299 * r + 0.587 * g + 0.114 * b; |
| 9 | pixels[i] = pixels[i + 1] = pixels[i + 2] = gray; |
| 10 | // alpha (i + 3) remains unchanged |
| 11 | } |
| 12 | return imageData; |
| 13 | } |
| 14 | |
| 15 | // Efficient data copy between typed arrays |
| 16 | const src = new Uint8Array([1, 2, 3, 4, 5]); |
| 17 | const dst = new Uint8Array(5); |
| 18 | dst.set(src); // copy entire array |
| 19 | dst.set(src.subarray(0, 3), 2); // copy subarray at offset 2 |
| 20 | |
| 21 | // Working with subarrays (no copy — shares buffer) |
| 22 | const big = new Uint8Array(1000); |
| 23 | const chunk = big.subarray(100, 200); // views same buffer |
| 24 | console.log(chunk.buffer === big.buffer); // true |
| 25 | console.log(chunk.byteOffset); // 100 |
| 26 | console.log(chunk.byteLength); // 100 |
| 27 | |
| 28 | // Fill and copyWithin |
| 29 | const arr = new Uint8Array(8); |
| 30 | arr.fill(0xFF); // all bytes set to 255 |
| 31 | arr.copyWithin(2, 0, 2); // copy first 2 bytes to offset 2 |
| 32 | console.log(arr); // [255, 255, 255, 255, 0, 0, 0, 0] |
| 33 | |
| 34 | // Sorting typed arrays — in-place |
| 35 | const scores = new Float64Array([3.2, 1.5, 4.8, 2.1]); |
| 36 | scores.sort(); |
| 37 | console.log(scores); // [1.5, 2.1, 3.2, 4.8] |
pro tip
best practice