67

I am moving from Node.js to browser environment, and I am still confused over ArrayBuffer vs. typed arrays (such as Uint8Array).

I am confused over where to use the typed arrays, and where to use ArrayBuffer directly. It's not hard to convert one to the other and vice versa, but which to use when?

For example, when I am creating an object that will represent a chunk of data in my code, should it be ArrayBuffer or Uint8Array? What does it depend on?

Or: should I rather return ArrayBuffer from my functions (for example, for external API), or the typed arrays?

Note that I can google how exactly to add elements etc to those typed arrays; what I am missing is some short general guide what to use where. Especially when moving from node's Buffer.

Karel Bílek
  • 36,467
  • 31
  • 94
  • 149
  • I know this question might be too broad, but I am not sure how to ask better :( – Karel Bílek Feb 23 '17 at 13:13
  • 2
    [Have you read the documentation?](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer) You don't ever "use ArrayBuffer directly". It's an abstraction representing the storage behind a typed array. – Pointy Feb 23 '17 at 13:16
  • 2
    But you can create an ArrayBuffer... `var buffer = new ArrayBuffer(8)`, as in the linked documentation. – Karel Bílek Feb 23 '17 at 13:18
  • Should I rather return `ArrayBuffer` from my functions (for example, for external API), or the typed arrays? – Karel Bílek Feb 23 '17 at 13:19
  • Well, very specific cases... e.g: when you use webgl, you will need typed arrays when drawing indexed data, Float32Array and Uint16Array are necessary here: https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Tutorial/Creating_3D_objects_using_WebGL – Leo Feb 23 '17 at 13:45
  • When you need to read some data from a specific server binary file that you created (I made this a year ago, to reduce data sent to browser), but like @Pointy says... "those things aren't mandatory". – Leo Feb 23 '17 at 13:48

2 Answers2

127

Concepts

ArrayBuffers represents a byte-array in physical memory. An ArrayBuffer is the actual storage for the bytes but is rarely used directly - in fact, you don't have access to read content of ArrayBuffer directly and can only pass a reference for it. They are on the other hand used for binary data transfers between server and client, or from the user's file system via Blobs.

ArrayBuffer byte array in memory
ArrayBuffer byte array in memory - each index equals one byte. ArrayBuffer is aligned in memory.

To read the content of an ArrayBuffer you need to use a view. This sits on top and offers an "api" to access the bytes by different width types, or arbitrarily.

Width-dependent Views

The different views are used depending on what you need. If you only need to read byte values, ie. signed values between -128 and 127 -or- unsigned values between 0-255, you would use Int8Array or Uint8Array. Notice that their names are a bit "misleading" as they are views and not arrays, and only references the underlying ArrayBuffer.

Likewise, you have views for Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array and Float64Array.

With the exception of *int8Arrays the others come with some requirement to ArrayBuffer size. For example, a Uint32Array view must sit on top of an ArrayBuffer that is divisible by four, otherwise it throws an error. *int 16 views would require a two-byte boundary.

This is usually not a problem because you can specify number of indexes using the view's constructor directly and a matching ArrayBuffer will be created automatically for it fulfilling these requirements.

And since the ArrayBuffer is a byte-array a *int16 view reads two bytes from it - or, one index = two bytes, *int32 four, or one index = four bytes, and so on.

The main difference between Uint8Array and Uint8ClampedArray is that values outside the range are subject to modulo (for example 256 becomes 0) with the ordinary arrays. In the clamped array the values are as suggested clamped instead (256 becomes 255).

*int16 view
Int16/Uint16 views - each index represents two bytes and is memory aligned.

*int32 view
Int32/Uint32 and Float32 views - each index represents four bytes and is memory aligned.

Float64 view
Float64 view - each index represents eight bytes and is memory aligned.

DataView for flexibility

Then there is the DataView. This is intended for scenarios where you need a flexible ArrayBuffer and need to read variable widths and from positions in the buffer that is not necessarily width or memory aligned.

For example, a *int32 index will always point to a memory location that is dividable by four. A DataView on the other hand can read a Uint32 from say, position 5 and will take care of all the needed steps internally (bit shifting, masking etc.), but at the cost of a tiny overhead.

One other difference is that a DataView doesn't use indexes but absolute byte-positions for the data it represents, and it comes with its own methods to read or write various widths from/to any position.

DataView
DataView - can read from any position and any width.

In other cases you can use several different views referencing the same underlying ArrayBuffer.

There is currently not 64-bits views for integer numbers, but seem to be proposed for ES8.

SharedArrayBuffers

It's also useful to mention the new SharedArrayBuffers that can be used across web workers.

You could (and still can) use transferable objects in the past in some browsers, but SharedArrayBuffers is more efficient in the sense the memory stays the same, only information about it is transferred. SharedArrayBuffers cannot become detached as ArrayBuffers can.

Purpose and Usage areas

Typed arrays are good to store specific numeric values and are fast. Bitmaps is a typical candidate for typed arrays (e.g. canvas 2D/WebGL).

Heavy data processing of data inside web workers is another use and so on. I already mentioned binary transfer between client and server or the file-system.

DataViews are perfect to parse or build binary files and file formats.

Typed arrays are an excellent way to pack binary data for sending over the net, to server or via web sockets and things like data-channels for WebRTC.

If you deal with audio, video, canvas, or media recording, there is often no way around using typed arrays.

The keys for using typed arrays are performance and memory. They are most often used in special scenarios, but there is nothing wrong using them in ordinary cases when you only need to store numeric values (or utf-8 strings, encryption vectors etc.). They are fast and have a low memory footprint.

Precautions

There are a couple of precautions to be aware of:

Byte-order

Some precautions must be made in regards to byte-order. Typed arrays always reflects the CPU-architecture they run under, ie. little-endian or big-endian. Most consumer systems are little-endian but when using *int16 and *int32 arrays you must pay special attention to byte-order. DataView can help with this part too, but is not always a good choice if performance is important.

Byte-order is also important when receiving data from server. They are usually always in big-endian format (AKA "network order"). For parsing file formats the same will apply.

Floating Point Number Encoding

Float32/Float64 will read and write numbers encoded in the IEEE-754 format. This is also something to be aware of if several views are used for the same buffer.

Cross-browser Support

Most browsers supports typed arrays nowadays. If you have to deal with older browsers you have to go back to IE9 or older mobile browsers to not be able to use them.

Safari is not particular optimized in regards to their performance, but the other benefits are there. Version 5.1 does not support Float64.

Mobile devices has their own hardware limitations, but in general: typed arrays are safe to use. For special cases there exist a polyfill.

Philipp Claßen
  • 41,306
  • 31
  • 146
  • 239
  • 4
    Thanks for an amazing answer. SO, there is no reason to use ArrayBuffer correctly, right. Great. – Karel Bílek Feb 24 '17 at 14:20
  • 26
    `const memory = new ArrayBuffer(2); const view = new Uint8Array(memory); view[0] = 0xFF; console.log(new Uint8Array(memory)); // Uint8Array(2) [255, 0]`. So basically an `ArrayBuffer` is actual memory, a `DataView` is its wrapper to access the memory in units of various sizes, a _Typed array_ is same as a `DataView` except that it accesses in a fixed-sized unit. Thank you. – Константин Ван Dec 28 '17 at 07:47
  • 6
    Is there any one around still understood nothing from this and looking for other resources.....(LIKE ME) – Nambi N Rajan Jun 13 '20 at 16:17
  • 5
    This does not answer at all the OP's question of whether one should pass around and use `ArrayBuffer` or a view on it like `Uint8Array`; if there are caveats to using one over the other... – 0__ Oct 30 '20 at 13:51
  • _Notice that their names are a bit "misleading" as they are views and not arrays, and only references the underlying ArrayBuffer._ Is it just an opinion, or does this relate to some older version of the spec? I'm aware that **one** of the constructors (namely `new UintXArray(buffer[, byteOffset[, length]])`) creates an array being a view to an `ArrayBuffer`, but other constructors do not. I didn't know that the `UintXArray` class as a whole is strictly tight to `ArrayBuffer` concept. – cubuspl42 Jun 11 '21 at 09:57
  • @cubuspl42 you are right that UintXArray is not tied to ArrayBuffer and is more than viewable. See the top example "from a length" for a straightforward demo: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array#different_ways_to_create_a_uint8array – GViz Jun 28 '21 at 18:24
3

I prefer to use TypedArray in function parameters and return type. A TypedArray could represent a part view of an ArrayBuffer, which means it might not be the same size of the underlying buffer. For example:

const buff = new ArrayBuffer(12);

// it will have the bytes from offset 4 to 7 (included)
const arr = new Uint8Array(buff, 4, 4);

The part view of the data is your function's concern, not the whole underlying buffer.

And what if you choose passing ArrayBuffer into the function in this case? Then you have to create a new ArrayBuffer, which is complex and performance harmful:

const buff = new ArrayBuffer(12);
foo(new Uint8Array(buff).slice(4, 8).buffer)

function foo(buff: ArrayBuffer) {
}

But, if you use TypedArray, be careful when you wanna write bytes into storage (like indexedDB). Be sure the TypedArray is align with the underlying buffer, otherwise you might write the whole data into the storage.


You could also choose to accept both ArrayBuffer and TypedArray if you like:

function foo(data: ArrayBufferView | ArrayBuffer) {
  // Convert to a view, or any TypedArray you want
  if(!ArrayBuffer.isView(data)) data = new Uint8Array(data); 
  // ...
}
tianjianchn
  • 431
  • 5
  • 7