Mémoire et performance JS/TS - 12 - ArrayBuffer et TypedArrays

Manipuler des donnees binaires en JavaScript avec ArrayBuffer, TypedArrays et DataView. Performances et cas d'usage.

  1. 01 Mémoire et performance JS/TS - 00 - Pourquoi la mémoire compte meme avec un garbage collector
  2. 02 Mémoire et performance JS/TS - 01 - Stack vs Heap
  3. 03 Mémoire et performance JS/TS - 02 - Le cycle de vie de la mémoire
  4. 04 Mémoire et performance JS/TS - 03 - Le garbage collector
  5. 05 Mémoire et performance JS/TS - 04 - V8 en profondeur
  6. 06 Mémoire et performance JS/TS - 05 - Les 6 fuites mémoire classiques
  7. 07 Mémoire et performance JS/TS - 06 - Closures et mémoire
  8. 08 Mémoire et performance JS/TS - 07 - WeakRef, WeakMap et WeakSet
  9. 09 Mémoire et performance JS/TS - 08 - FinalizationRegistry : savoir quand le GC passe
  10. 10 Mémoire et performance JS/TS - 09 - DevTools Memory : investiguer dans Chrome
  11. 11 Mémoire et performance JS/TS - 10 - Profiling mémoire en Node.js
  12. 12 Mémoire et performance JS/TS - 11 - Détecter et corriger les fuites mémoire
  13. 13 Mémoire et performance JS/TS - 12 - ArrayBuffer et TypedArrays
  14. 14 Mémoire et performance JS/TS - 13 - Workers et mémoire partagee
  15. 15 Mémoire et performance JS/TS - 14 - Streams et backpressure
  16. 16 Mémoire et performance JS/TS - 15 - Fuites mémoire en React
  17. 17 Mémoire et performance JS/TS - 16 - Serveurs Node.js et mémoire
  18. 18 Mémoire et performance JS/TS - 17 - Mémoire et Docker
  19. 19 Mémoire et performance JS/TS - 18 - Optimisations mémoire
  20. 20 Mémoire et performance JS/TS - 19 - Comparaison avec d'autres langages
  21. 21 Mémoire et performance JS/TS - 20 - Tester la mémoire
  22. 22 Mémoire et performance JS/TS - 21 - Glossaire

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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.