JavaScript Typed Arrays Table
| Piece | What it does | Field note |
|---|---|---|
new Uint8Array(8) | Fixed-type array view | One type per array, no boxing - the fast dense numbers |
ArrayBuffer | The raw byte slab | No direct access - views read it; fixed length forever |
DataView | Byte-precise read/write | getUint32(offset, littleEndian) - file format parsing's tool |
Typed array OF a buffer | View, not copy | const v = new Uint8Array(buffer) shares the bytes - writes show through |
subarray() | Window into the data | Shares memory - slice() copies, subarray() does not |
TextEncoder / TextDecoder | Strings to bytes, back | UTF-8 default - the WebSocket/file boundary pair |
Blob / File | Binary with a MIME | Feeds img src, download links, FormData - the user-facing wrapper |
transfer to worker | Zero-copy move | ArrayBuffers transfer (sender loses) - big payload's only fast path |
Binary JavaScript has three layers: the ArrayBuffer is the raw byte slab (no direct reads, fixed length), typed arrays are TYPED VIEWS over it (Uint8Array, Float32Array - dense, unboxed, math-fast), and DataView is the byte-precise reader for formats with mixed-width fields and endianness.
Bottom line: views SHARE their buffer. Two typed arrays over one ArrayBuffer see each other's writes; subarray() is a window (no copy), slice() is a copy. Every memory bug in binary JS traces to forgetting which side of that line an API sits on.
The honest part: regular Arrays are object arrays - flexible but boxed and sparse. Typed arrays trade that flexibility for contiguous memory and real numeric types, which is what ImageData, WebGL buffers, WebAudio samples, WebSocket frames and file parsing are made of. If you are not touching binary data, you do not need them.
How to use
- Bridge strings at the boundary: new TextEncoder().encode(str) gives UTF-8 bytes; new TextDecoder().decode(bytes) returns - the WebSocket and file-format pair.
- Parse a binary format with DataView: dv.getUint32(0, false) reads a big-endian 32-bit int at offset 0 - explicit endianness per field beats guessing.
- Ship bytes to a worker fast: worker.postMessage(buffer, [buffer]) - the TRANSFER list moves ownership in O(1); treat the sender's reference as dead.
Frequently asked questions
What is the relationship between an ArrayBuffer and a typed array?
Memory versus window. The ArrayBuffer is a fixed-length block of raw bytes with NO read/write API of its own; a typed array (or DataView) is a VIEW that interprets those bytes as typed values. new Uint8Array(buffer) does not copy - it looks at the same bytes through an unsigned-8-bit lens, so writing v[0] = 255 is visible to every other view on that buffer, and to a DataView reading it as a 32-bit int. This is also how you reinterpret data cheaply: one buffer, a Float32Array view for math and a Uint8Array view for storage - the views are the API, the buffer is the memory.
When is subarray() right versus slice()?
subarray() creates a new VIEW over the same bytes: zero-copy, instant, but any write through the subarray lands in the original - and keeping the subarray alive keeps the WHOLE original buffer alive (a small window pins a large slab from garbage collection). slice() copies the bytes out: independent, safe to hold long-term, costs memory and time proportional to length. The rule: temporary parsing windows take subarray(); data that escapes your function, gets stored, or must not alias takes slice(). The bug shape is always the same - a cached subarray silently mutating (or pinning) the source.
Why does TextEncoder exist when strings feel like data already?
Because a JavaScript string is UTF-16 code units in memory, while every boundary - files, WebSockets, fetch bodies, crypto - speaks BYTES with an encoding. TextEncoder.encode('héllo') produces UTF-8 bytes (6 bytes here, not 5); TextDecoder decodes the reverse, and it handles invalid byte sequences with explicit policies rather than silent mojibake. Note the asymmetry worth knowing: encode() is stateless and reusable, while decode() is stateful for STREAMING - multi-byte characters split across chunks need decode(value, { stream: true }) on each piece.
What makes transferring an ArrayBuffer to a worker O(1)?
Ownership moves instead of data. Normally postMessage structured-clones the buffer - a byte-for-byte copy proportional to size (100MB payload = 100MB copy). The transfer list (the second postMessage argument) instead reassigns the underlying memory to the worker's realm: zero copying, microseconds, but the SENDER's reference is neutered (byteLength becomes 0, further reads fail) - a move, not a share. The pipeline pattern: read a file into a buffer on the main thread, transfer to the worker for heavy parsing, transfer the result buffer back. For WebAssembly-heavy apps this zero-copy discipline is the difference between snappy and dead.