Typed Arrays & ArrayBuffer

Difficulty: Advanced

Typed arrays and ArrayBuffer provide JavaScript with the ability to work with raw binary data efficiently. While regular JavaScript arrays are dynamic, heterogeneous collections that store values as boxed objects, typed arrays are fixed-length views over contiguous binary memory. This design makes them dramatically faster for numerical computations and essential for working with binary protocols, WebGL, audio processing, and file I/O.

ArrayBuffer is the foundation - it represents a fixed-length block of raw binary memory. You cannot directly read or write to an ArrayBuffer; instead, you create a 'view' over it using either a typed array or a DataView. Think of ArrayBuffer as a chunk of raw bytes and views as the lenses through which you interpret those bytes as meaningful numbers. A single ArrayBuffer can have multiple views of different types, all sharing the same underlying memory.

Typed arrays come in several varieties, each interpreting the underlying bytes differently. Int8Array and Uint8Array treat each byte as a signed or unsigned 8-bit integer. Int16Array and Uint16Array use pairs of bytes for 16-bit integers. Int32Array, Uint32Array, and Float32Array use four bytes each. Float64Array uses eight bytes for double-precision floating point. BigInt64Array and BigUint64Array (ES2020) handle 64-bit integers. The choice of typed array depends on the precision and range you need for your data.

DataView provides a more flexible way to read and write binary data. Unlike typed arrays which enforce a single element type, DataView lets you read or write different types at different byte offsets within the same buffer. It also lets you control byte order (endianness) - whether multi-byte values are stored with the most significant byte first (big-endian) or last (little-endian). This flexibility is crucial when working with binary protocols that specify a particular byte order.

The primary use cases for typed arrays are performance-critical applications that handle large amounts of numerical data. WebGL uses Float32Array for vertex positions, colors, and transformation matrices. The Web Audio API uses Float32Array for audio sample data. Image processing works with Uint8ClampedArray (which clamps values to 0-255) for pixel data. Network protocols and file formats often define binary structures that map naturally to typed arrays.

SharedArrayBuffer is a variant of ArrayBuffer that can be shared between the main thread and Web Workers, enabling true shared-memory parallelism in JavaScript. Combined with Atomics (which provides atomic operations like compareExchange and wait/notify), SharedArrayBuffer allows lock-free data structures and inter-thread communication. Due to Spectre-class vulnerabilities, SharedArrayBuffer requires specific security headers (COOP and COEP) to be enabled.

Code examples

ArrayBuffer and typed array basics

// Create an ArrayBuffer of 16 bytes
const buffer = new ArrayBuffer(16);
console.log('Buffer size:', buffer.byteLength, 'bytes');

// Create different views over the same buffer
const int8View = new Int8Array(buffer);
const int16View = new Int16Array(buffer);
const int32View = new Int32Array(buffer);
const float32View = new Float32Array(buffer);

console.log('Int8 elements:', int8View.length);     // 16 bytes / 1
console.log('Int16 elements:', int16View.length);   // 16 bytes / 2
console.log('Int32 elements:', int32View.length);   // 16 bytes / 4
console.log('Float32 elements:', float32View.length); // 16 bytes / 4

// Writing through one view affects others (shared memory)
int32View[0] = 1;
int32View[1] = 256;
console.log('\nInt32:', [...int32View]);
console.log('Int8:', [...int8View]);

// Create typed arrays directly
const scores = new Float32Array([98.5, 87.3, 92.1, 75.8]);
console.log('\nScores:', [...scores]);
console.log('Sum:', scores.reduce((a, b) => a + b).toFixed(1));

// Typed array from length
const pixels = new Uint8Array(4); // RGBA
pixels[0] = 255; // R
pixels[1] = 128; // G
pixels[2] = 0;   // B
pixels[3] = 255; // A
console.log('Pixel RGBA:', [...pixels]);

Different typed array views interpret the same underlying bytes differently. Int32View[1] = 256 appears as [0, 1, 0, 0] in Int8View because 256 in little-endian is 0x00000100. Float32 shows precision artifacts because 32-bit floats can't represent all decimal values exactly.

DataView for flexible binary access

// DataView allows reading different types at different offsets
const buffer = new ArrayBuffer(12);
const view = new DataView(buffer);

// Write a mix of types into the buffer
view.setUint8(0, 72);           // byte 0: 'H' char code
view.setUint8(1, 105);          // byte 1: 'i' char code
view.setInt16(2, 1000, true);   // bytes 2-3: signed 16-bit (little-endian)
view.setFloat32(4, 3.14, true); // bytes 4-7: 32-bit float (little-endian)
view.setUint32(8, 42, true);    // bytes 8-11: unsigned 32-bit (little-endian)

// Read back
console.log('Char 1:', String.fromCharCode(view.getUint8(0)));
console.log('Char 2:', String.fromCharCode(view.getUint8(1)));
console.log('Int16:', view.getInt16(2, true));
console.log('Float32:', view.getFloat32(4, true).toFixed(2));
console.log('Uint32:', view.getUint32(8, true));

// Simulating a binary protocol header
const headerBuf = new ArrayBuffer(8);
const header = new DataView(headerBuf);

// Protocol: [version:u8] [type:u8] [length:u16] [checksum:u32]
header.setUint8(0, 2);               // version 2
header.setUint8(1, 0x0A);            // message type
header.setUint16(2, 1024, false);    // big-endian length
header.setUint32(4, 0xDEADBEEF, false); // big-endian checksum

console.log('\n--- Protocol Header ---');
console.log('Version:', header.getUint8(0));
console.log('Type:', '0x' + header.getUint8(1).toString(16));
console.log('Length:', header.getUint16(2, false));
console.log('Checksum:', '0x' + header.getUint32(4, false).toString(16));

DataView reads/writes different types at specific byte offsets. The second argument to multi-byte methods controls endianness (true = little-endian, false = big-endian). This flexibility is essential for implementing binary protocols with mixed field types.

Typed array methods and operations

// Typed arrays have most Array methods
const data = new Float64Array([5.2, 3.1, 8.7, 1.4, 6.9]);

// Sorting
const sorted = new Float64Array(data);
sorted.sort();
console.log('Original:', [...data]);
console.log('Sorted:', [...sorted]);

// map, filter via Array.from
const doubled = Float64Array.from(data, x => x * 2);
console.log('Doubled:', [...doubled]);

// subarray (shares memory - no copy!)
const sub = data.subarray(1, 4);
console.log('Subarray:', [...sub]);
sub[0] = 999;
console.log('Original after sub[0]=999:', [...data]);

// slice (creates a copy)
const copy = data.slice(1, 4);
copy[0] = 0;
console.log('Original after copy[0]=0:', [...data]);

// set: copy values from another array
const target = new Uint8Array(8);
target.set([10, 20, 30], 0);       // at offset 0
target.set([40, 50], 5);            // at offset 5
console.log('\nTarget:', [...target]);

// Typed array type info
console.log('\nBYTES_PER_ELEMENT:');
console.log('  Uint8Array:', Uint8Array.BYTES_PER_ELEMENT);
console.log('  Int16Array:', Int16Array.BYTES_PER_ELEMENT);
console.log('  Float32Array:', Float32Array.BYTES_PER_ELEMENT);
console.log('  Float64Array:', Float64Array.BYTES_PER_ELEMENT);
console.log('  BigInt64Array:', BigInt64Array.BYTES_PER_ELEMENT);

subarray() creates a view sharing the same buffer - modifying the subarray modifies the original. slice() copies the data. The set() method efficiently copies values at a specified offset. BYTES_PER_ELEMENT reveals how many bytes each element occupies.

Practical: binary file parsing and SharedArrayBuffer intro

// Simulating reading a BMP-like file header
function parseBitmapHeader(bytes) {
  const view = new DataView(bytes.buffer);
  
  return {
    signature: String.fromCharCode(view.getUint8(0), view.getUint8(1)),
    fileSize: view.getUint32(2, true),     // little-endian
    width: view.getInt32(6, true),
    height: view.getInt32(10, true),
    bitsPerPixel: view.getUint16(14, true)
  };
}

// Create a fake bitmap header
const headerBytes = new Uint8Array(16);
const headerView = new DataView(headerBytes.buffer);
headerView.setUint8(0, 66);            // 'B'
headerView.setUint8(1, 77);            // 'M'
headerView.setUint32(2, 921654, true); // file size
headerView.setInt32(6, 1920, true);    // width
headerView.setInt32(10, 1080, true);   // height
headerView.setUint16(14, 24, true);    // 24-bit color

const header = parseBitmapHeader(headerBytes);
console.log('Bitmap Header:');
console.log('  Signature:', header.signature);
console.log('  File size:', header.fileSize, 'bytes');
console.log('  Dimensions:', header.width, 'x', header.height);
console.log('  Color depth:', header.bitsPerPixel, 'bit');

// Performance comparison: typed vs regular array
const SIZE = 100000;
console.log('\n--- Performance (', SIZE, 'elements) ---');

console.time('Regular Array');
const regArr = new Array(SIZE);
for (let i = 0; i < SIZE; i++) regArr[i] = Math.random();
let regSum = 0;
for (let i = 0; i < SIZE; i++) regSum += regArr[i];
console.timeEnd('Regular Array');

console.time('Float64Array');
const typedArr = new Float64Array(SIZE);
for (let i = 0; i < SIZE; i++) typedArr[i] = Math.random();
let typedSum = 0;
for (let i = 0; i < SIZE; i++) typedSum += typedArr[i];
console.timeEnd('Float64Array');

// SharedArrayBuffer concept
console.log('\n--- SharedArrayBuffer ---');
console.log('SharedArrayBuffer enables shared memory between threads');
console.log('Requires COOP/COEP security headers in browsers');
console.log('Use Atomics for safe concurrent access');

Binary file parsing maps naturally to DataView - each field is read at its byte offset with the correct type and endianness. Typed arrays are faster than regular arrays for numerical operations due to contiguous memory layout and no type boxing. SharedArrayBuffer extends this to multi-threaded scenarios.

Key points

Concepts covered

ArrayBuffer, DataView, typed arrays, Int8Array, Uint8Array, Float32Array, binary data, SharedArrayBuffer