12 - ArrayBuffer et TypedArrays
Ce que tu vas apprendre
- Ce qu'est un ArrayBuffer et pourquoi il existe
- Les TypedArrays : Uint8Array, Float32Array, Int16Array et les autres
- DataView pour lire des formats binaires complexes
- SharedArrayBuffer pour le partage entre Workers
- Les performances comparees aux tableaux classiques
- Les cas d'usage : WebGL, parsing de fichiers, protocoles réseau
Prerequisites
Avoir lu l'article 11 sur la détection de fuites mémoire.
JavaScript a ete conçu pour manipuler du texte et des objets dans un navigateur. Pas pour lire des fichiers binaires, traiter des images pixel par pixel ou envoyer des paquets réseau bruts. Puis WebGL est arrive, et avec lui le besoin de manipuler des blocs de mémoire brute. C'est de la qu'ArrayBuffer est ne.
ArrayBuffer : le bloc de mémoire brute
Un ArrayBuffer est un bloc contigu d'octets. Tu ne peux pas lire ou écrire directement dedans. C'est juste de la mémoire allouee, un peu comme un malloc en C.
typescript// Allouer 16 octets
const buffer = new ArrayBuffer(16);
console.log(buffer.byteLength); // 16
Pour lire ou écrire dans ce buffer, tu as besoin d'une "vue" : un TypedArray ou un DataView.
TypedArrays : des vues typees
Un TypedArray est une vue sur un ArrayBuffer qui interprète les octets selon un type precis :
typescriptconst buffer = new ArrayBuffer(16);
// Vue en entiers 32 bits (4 octets chacun) -> 4 elements
const int32 = new Int32Array(buffer);
int32[0] = 42;
int32[1] = -1;
// Vue en octets sur le meme buffer -> 16 elements
const uint8 = new Uint8Array(buffer);
console.log(uint8[0]); // 42 (octet de poids faible de int32[0])
Les deux vues pointent vers le meme buffer. Modifier l'une modifie l'autre. C'est le meme bloc de mémoire, lu differemment.
Les TypedArrays disponibles :
| Type | Taille (octets) | Plage |
|---|---|---|
| Int8Array | 1 | -128 a 127 |
| Uint8Array | 1 | 0 a 255 |
| Uint8ClampedArray | 1 | 0 a 255 (clamped) |
| Int16Array | 2 | -32768 a 32767 |
| Uint16Array | 2 | 0 a 65535 |
| Int32Array | 4 | -2^31 a 2^31-1 |
| Uint32Array | 4 | 0 a 2^32-1 |
| Float32Array | 4 | flottant simple precision |
| Float64Array | 8 | flottant double precision |
| BigInt64Array | 8 | -2^63 a 2^63-1 |
| BigUint64Array | 8 | 0 a 2^64-1 |
Uint8ClampedArray est special : au lieu de deborder, les valeurs sont clampees entre 0 et 255. C'est ce que l'API Canvas utilise pour les pixels.
Creer des TypedArrays
Plusieurs facons :
typescript// A partir d'un ArrayBuffer existant
const buffer = new ArrayBuffer(12);
const view = new Float32Array(buffer); // 3 floats de 4 octets
// Directement avec une taille
const pixels = new Uint8Array(1024); // 1024 octets, initialises a 0
// A partir d'un tableau
const data = new Float32Array([1.5, 2.7, 3.14]);
// A partir d'un autre TypedArray
const copy = new Uint8Array(new Uint8Array([10, 20, 30]));
// Vue partielle d'un buffer (offset et longueur)
const partial = new Int16Array(buffer, 4, 2); // offset 4 octets, 2 elements
DataView : lecture flexible
Les TypedArrays imposent un type uniforme. DataView permet de lire des types différents a des offsets arbitraires. Indispensable pour parser des formats binaires :
typescriptconst buffer = new ArrayBuffer(12);
const view = new DataView(buffer);
// Ecrire un header : magic number (uint32) + version (uint16) + flags (uint16) + size (float32)
view.setUint32(0, 0x504e47, false); // big-endian
view.setUint16(4, 1, false);
view.setUint16(6, 0b1010, false);
view.setFloat32(8, 1024.5, false);
// Relire
const magic = view.getUint32(0, false);
const version = view.getUint16(4, false);
const size = view.getFloat32(8, false);
Le dernier paramètre est le little-endian flag. false = big-endian (réseau), true = little-endian (la plupart des CPU modernes). Avec les TypedArrays, l'endianness suit celle de la plateforme. Avec DataView, tu la choisis explicitement. Pour les protocoles réseau, utilise toujours DataView.
Cas d'usage concrets
Manipulation d'image
typescript// Lire les pixels d'un canvas
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d")!;
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data; // Uint8ClampedArray [R,G,B,A, R,G,B,A, ...]
// Convertir en niveaux de gris
for (let i = 0; i < pixels.length; i += 4) {
const gray = pixels[i] * 0.299 + pixels[i + 1] * 0.587 + pixels[i + 2] * 0.114;
pixels[i] = gray; // R
pixels[i + 1] = gray; // G
pixels[i + 2] = gray; // B
// pixels[i + 3] est l'alpha, on ne touche pas
}
ctx.putImageData(imageData, 0, 0);
Parser un fichier binaire
typescriptasync function parseBMP(file: File): Promise<{ width: number; height: number }> {
const buffer = await file.arrayBuffer();
const view = new DataView(buffer);
// Header BMP : offset 18 = largeur (int32 LE), offset 22 = hauteur (int32 LE)
const width = view.getInt32(18, true);
const height = view.getInt32(22, true);
return { width, height: Math.abs(height) };
}
Protocole réseau
typescriptfunction encodePacket(type: number, payload: Uint8Array): ArrayBuffer {
const buffer = new ArrayBuffer(4 + payload.length);
const view = new DataView(buffer);
view.setUint16(0, type, false); // type sur 2 octets, big-endian
view.setUint16(2, payload.length, false); // longueur sur 2 octets
const body = new Uint8Array(buffer, 4);
body.set(payload);
return buffer;
}
SharedArrayBuffer
SharedArrayBuffer est un ArrayBuffer qui peut etre partage entre le thread principal et les Workers. Contrairement a postMessage qui copie les donnees, SharedArrayBuffer donne acces a la meme zone mémoire :
typescript// Thread principal
const shared = new SharedArrayBuffer(1024);
const view = new Int32Array(shared);
view[0] = 42;
worker.postMessage(shared); // Pas de copie !
// Dans le Worker
onmessage = (e) => {
const view = new Int32Array(e.data);
console.log(view[0]); // 42 - meme memoire
};
Attention : SharedArrayBuffer nécessité des headers COOP/COEP pour fonctionner dans le navigateur (a cause de Spectre). On verra les détails dans l'article 13 sur les Workers.
Alignement mémoire
Les TypedArrays doivent etre alignes sur la taille de leur type. Un Float32Array a besoin d'un offset multiple de 4 :
typescriptconst buffer = new ArrayBuffer(16);
// OK : offset 0 (multiple de 4)
const f1 = new Float32Array(buffer, 0, 1);
// OK : offset 4 (multiple de 4)
const f2 = new Float32Array(buffer, 4, 1);
// ERREUR : offset 3 (pas multiple de 4)
// const f3 = new Float32Array(buffer, 3, 1); // RangeError
Un Uint8Array n'a aucune contrainte d'alignement (1 octet). Un Int16Array a besoin d'un offset pair.
Performance vs tableaux classiques
Les TypedArrays sont plus rapides que les Array classiques pour les opérations numériques. Pourquoi ? Parce qu'un Array JavaScript stocke des valeurs boxees (chaque nombre est un objet). Un TypedArray stocke des valeurs brutes, contiguees en mémoire.
typescript// Tableau classique : chaque element est une valeur JS boxee
const jsArray = new Array(1_000_000).fill(0);
// TypedArray : memoire contiguee, valeurs brutes
const typedArray = new Float64Array(1_000_000);
// Le TypedArray utilise exactement 8 Mo (8 octets * 1M)
// Le Array peut utiliser 2-3x plus a cause du boxing et de la structure interne
Pour du calcul numérique pur (traitement de signal, physique, rendu), les TypedArrays sont le bon choix. Pour du code métier classique avec des objets et des strings, les tableaux normaux font tres bien l'affaire.
Sur paltemps.fr, les TypedArrays servent au traitement d'images (compression, redimensionnement). Le reste du code utilise des tableaux classiques. Pas besoin de sur-optimiser là où ce n'est pas le goulot.
Résumé
- ArrayBuffer est un bloc de mémoire brute, les TypedArrays et DataView sont des vues dessus
- 11 types de TypedArrays, de Int8Array a BigUint64Array
- DataView permet de lire/écrire des types mixtes avec contrôle de l'endianness
- SharedArrayBuffer partage la mémoire entre threads (Workers)
- Les TypedArrays sont plus performants et compacts que les Array pour le calcul numérique
- Cas d'usage : images, fichiers binaires, protocoles réseau, WebGL
Article précédent : 11 - Détecter et corriger les fuites mémoire Article suivant : 13 - Workers et mémoire partagee