Core Idea
The wreck above is not corruption: both views read the same bytes, and the only difference is one boolean on getFloat32. Flip which end of each number is read first and the exponent bits land in the wrong place — a clean mesh becomes values spanning ±10³⁸. 0x12345678 is one number but four bytes, and memory is addressed one byte at a time — so the bytes have to land in some order. Endianness is that order. It is invisible for almost all programming: your code, your CPU, and your typed arrays all agree, so nothing ever looks flipped. It turns real the moment binary data crosses a system boundary — a file parsed in the browser, a binary WebSocket message, a mesh downloaded from a server — because then the machine that wrote the bytes and the machine reading them are free to disagree.
The Two Orders
- Little-endian — the least-significant byte goes to the lowest address; the number is stored “small end first”. Every CPU you’re likely to ship to today — x86, ARM, Apple Silicon — is little-endian.
- Big-endian — the most-significant byte goes first, the way you’d write the number on paper. TCP/IP standardized on it, which is why it’s also called network byte order; many file formats followed.
| Order | 0x12345678 in memory (low → high address) |
|---|---|
| Little-endian | 78 56 34 12 |
| Big-endian | 12 34 56 78 |
Neither is wrong — they’re conventions. The failure mode is mixing them, and it fails loudly in the worst way: no error, just a wrong number. Lay a value into memory below and read it back with the wrong flag:
A float doesn’t survive the flip any better than an integer — 1.0 read backwards isn’t -1.0 or 2.0, it’s 4.6006e-41, because the sign, exponent, and mantissa bits all land in the wrong fields. That “almost zero but not quite” garbage is the classic symptom of an endianness bug in mesh or texture parsing.
What JavaScript Does About It
The typed-array API splits the problem deliberately in two:
Float32Array, Uint16Array, … always use native CPU order. No flag, no conversion, no per-element cost — which is exactly what makes them the fast path for GPU work. By the time a buffer reaches gl.bufferData, it’s native-order memory and endianness is already resolved.
DataView is the boundary tool. Every multi-byte read takes an explicit flag, so parsing a file means stating the format’s byte order instead of inheriting the machine’s:
const dv = new DataView(arrayBuffer);
const x = dv.getFloat32(offset, true); // true = little-endian
const n = dv.getUint32(offset, false); // false = big-endian (the default)
The rule of thumb falls out directly:
- Binary arrives from a file or network → parse it once with
DataView, using the byte order the format specifies. - Store the results in a
TypedArrayfor the GPU (gl.bufferData,gl.texImage2D). - Never overlay an
Int16Array/Float32Arrayview directly onto downloaded bytes without knowing the source order — on the wrong machine it reads garbage silently.
Where It Bites
- Binary file formats — BMP headers, HDR texture data, and glTF’s binary
.glbchunks all specify little-endian; aDataViewparse honors that on any platform. - Binary WebSocket protocols — a custom game-server protocol must document its byte order; both ends parse with
DataView, not raw views. - Custom mesh formats — any hand-rolled binary geometry file needs its order stated once and respected forever.
- Where it doesn’t apply —
Uint8ClampedArrayand other single-byte views (canvas pixel channels) are immune: one byte has no order to argue about.
Connections
Endianness is the boundary condition of buffers: typed arrays keep bytes in native order precisely so the WebGL upload path (gl.bufferData) can be a straight memcpy, and DataView exists so that boundary — file or network in, native memory out — is crossed exactly once, explicitly.