Transferable Objects: ArrayBuffer verschieben statt kopieren bei postMessage
AI generated
JS
() =>
JavaScript · Web Worker · Performance
Transferable Objects: Speicher verschieben statt kopieren
ArrayBuffer.prototype.transfer() und postMessage-Transferlisten fuer schnellen Datenaustausch mit Web Workern

Structured Clone kopiert Daten standardmaessig bei jedem postMessage-Aufruf, was bei grossen Binaerdaten spuerbar Zeit kostet. Transferable Objects loesen das durch einen Ownership-Wechsel statt einer Kopie. Dieser Artikel zeigt die Transfer-Liste von postMessage, das neuere ArrayBuffer.prototype.transfer() und typische Fehlerquellen mit detached Buffers.

15 Min. Lesezeit Web Worker ArrayBuffer Performance

1. Das Problem: teures Kopieren grosser Binaerdaten

Wenn Daten per postMessage zwischen dem Main Thread und einem Web Worker ausgetauscht werden, nutzt der Browser standardmaessig den Structured Clone Algorithmus, der eine vollstaendige Kopie der uebergebenen Daten erstellt. Bei kleinen Objekten faellt das kaum ins Gewicht, bei grossen ArrayBuffers, etwa fuer Bild-, Audio- oder Sensordaten im Megabyte- bis Gigabyte-Bereich, wird die Kopie jedoch zu einem messbaren Performance-Problem.

Die Kosten des Kopierens skalieren direkt mit der Datengroesse und blockieren zudem kurzzeitig den Thread, der die Nachricht sendet. Genau fuer dieses Szenario wurde das Konzept der Transferable Objects eingefuehrt, das es erlaubt, den Speicher eines Objekts ohne Kopie an den Empfaenger zu uebergeben.

2. Structured Clone als Standardverhalten

Ohne explizite Transfer-Liste kopiert postMessage jeden uebergebenen ArrayBuffer vollstaendig. Das Ergebnis ist, dass sowohl der Main Thread als auch der Worker danach jeweils eine eigene, unabhaengige Kopie derselben Daten im Speicher halten, was den Speicherverbrauch kurzzeitig verdoppelt und die Uebertragungszeit proportional zur Groesse der Daten erhoeht.

Fuer die meisten Anwendungsfaelle mit kleinen Nachrichten ist dieses Verhalten voellig ausreichend und sogar sicherer, weil beide Seiten unabhaengig voneinander mit ihrer eigenen Kopie arbeiten koennen, ohne sich gegenseitig zu beeinflussen. Erst bei groesseren Datenmengen lohnt sich der Umstieg auf Transfer.


// Main Thread: Standard-Kopie ohne Transfer-Liste
const buffer = new ArrayBuffer(64 * 1024 * 1024); // 64 MB
const worker = new Worker("worker.js");

console.time("postMessage-copy");
worker.postMessage({ buffer });
console.timeEnd("postMessage-copy");
// Buffer bleibt im Main Thread vollstaendig nutzbar,
// der Worker erhaelt eine komplette Kopie.

3. Transferable Objects: Ownership-Wechsel statt Kopie

postMessage akzeptiert als zweiten Parameter eine Transfer-Liste, ein Array aus Objekten, deren zugrundeliegender Speicher nicht kopiert, sondern deren Ownership an den Empfaenger uebertragen wird. Nach dem Transfer besitzt ausschliesslich der Empfaenger Zugriff auf die Daten, der Sender verliert ihn vollstaendig.

Transferable sind unter anderem ArrayBuffer, MessagePort, ImageBitmap und OffscreenCanvas. TypedArrays wie Uint8Array selbst sind nicht direkt transferable, aber ihr zugrundeliegendes .buffer ist es, weshalb bei TypedArrays gezielt der buffer in die Transfer-Liste aufgenommen werden muss, nicht die View selbst. Wird versehentlich ein nicht-transferables Objekt in die Liste aufgenommen, wirft der Browser sofort einen DataCloneError, bevor ueberhaupt Daten uebertragen werden, was Fehlkonfigurationen fruehzeitig sichtbar macht.

4. Praktisches Beispiel: Transfer eines grossen Buffers

Im folgenden Beispiel wird ein grosser ArrayBuffer per Transfer-Liste an einen Worker uebergeben. Der entscheidende Unterschied zum Standardverhalten ist der zweite Parameter von postMessage, der explizit angibt, welche Objekte transferiert statt kopiert werden sollen.

Nach dem Aufruf ist der Buffer im Main Thread detached, seine byteLength betraegt 0 und jeder Lesezugriff auf die alten TypedArray-Views wirft keinen Fehler, liefert aber keine Daten mehr. Das ist ein bewusstes Sicherheitsmerkmal, das verhindert, dass zwei Threads gleichzeitig denselben Speicherbereich unkontrolliert veraendern.


// Main Thread
const buffer = new ArrayBuffer(64 * 1024 * 1024);
const worker = new Worker("worker.js");

worker.postMessage({ buffer }, [buffer]);

console.log(buffer.byteLength); // -> 0, Buffer ist jetzt detached

// worker.js
self.onmessage = (event) => {
  const { buffer } = event.data;
  console.log(buffer.byteLength); // -> 67108864, voller Zugriff im Worker
};

5. ArrayBuffer.prototype.transfer(): expliziter Transfer im selben Kontext

Mit ECMAScript 2024 kam ArrayBuffer.prototype.transfer() hinzu, eine Methode, die einen neuen ArrayBuffer erzeugt, der den Speicherinhalt des urspruenglichen Buffers uebernimmt, waehrend der urspruengliche Buffer sofort detached wird. Anders als der postMessage-Transfer funktioniert das auch innerhalb desselben Threads, etwa um einen Buffer gezielt an eine andere Funktion weiterzureichen, ohne dass die aufrufende Funktion danach noch Zugriff behaelt.

Die Variante transferToFixedLength() erzwingt zusaetzlich, dass der neue Buffer nicht mehr resizable ist, selbst wenn der urspruengliche Buffer mit maxByteLength als resizable erstellt wurde. Beide Methoden akzeptieren optional eine neue Byte-Laenge als Parameter, wodurch der resultierende Buffer beim Transfer gleichzeitig vergroessert oder verkleinert werden kann.


const original = new ArrayBuffer(16);
new Uint8Array(original).set([1, 2, 3, 4]);

const moved = original.transfer(); // Ownership wechselt
console.log(original.byteLength);  // -> 0, original ist detached
console.log(moved.byteLength);     // -> 16, Daten sind uebernommen
console.log(new Uint8Array(moved)); // -> Uint8Array(16) [1, 2, 3, 4, 0, ...]

6. Detached Buffers als haeufige Fehlerquelle

Der haeufigste Bug rund um Transferable Objects entsteht, wenn nach dem Transfer noch versehentlich auf die alte Referenz zugegriffen wird, etwa weil ein TypedArray-View auf den alten Buffer in einer Closure zwischengespeichert wurde. Der Zugriff wirft zwar keinen Laufzeitfehler, liefert aber stillschweigend leere oder unerwartete Daten, was das Debugging erschwert.

Ein guter Debugging-Ansatz ist, vor jedem Transfer explizit zu pruefen und zu loggen, ob ein Buffer bereits detached ist, erkennbar an einer byteLength von 0 in Kombination mit dem Wissen, dass der Buffer zuvor eine groessere Laenge hatte. Konsequentes Aufraeumen aller Referenzen auf den alten Buffer direkt nach dem Transfer verhindert diese Klasse von Fehlern zuverlaessig.


function safeTransfer(buffer) {
  if (buffer.byteLength === 0) {
    throw new Error("Buffer ist bereits detached, kann nicht erneut transferiert werden");
  }
  return buffer.transfer();
}

const buf = new ArrayBuffer(8);
const moved = safeTransfer(buf);
try {
  safeTransfer(buf); // -> wirft, buf.byteLength ist bereits 0
} catch (error) {
  console.error(error.message);
}

7. Performance: wann sich der Umstieg lohnt

Bei kleinen Datenmengen im Kilobyte-Bereich ist der Unterschied zwischen Kopie und Transfer kaum messbar, der Aufwand fuer die explizite Transfer-Liste lohnt sich hier meist nicht. Ab einigen Megabyte Daten, etwa bei Video-Frames, Audio-Buffern oder groesseren Sensor-Datensaetzen, wird der Unterschied deutlich spuerbar, da Transfer nahezu konstante Zeit benoetigt statt linear mit der Groesse zu wachsen.

Bei sehr grossen Datenmengen im dreistelligen Megabyte- oder Gigabyte-Bereich kann eine Structured-Clone-Kopie ausserdem kurzzeitig zu spuerbaren Rucklern in der Benutzeroberflaeche fuehren, waehrend ein Transfer praktisch sofort abgeschlossen ist, da lediglich ein Zeiger auf denselben Speicherbereich uebergeben wird, keine Bytes tatsaechlich kopiert werden.

8. Praxisfall: Verarbeitungs-Pipeline aus mehreren Workern

Ein typischer Praxisfall ist die Bild- oder Audioverarbeitung in einer Pipeline aus mehreren Workern, bei der Rohdaten von einem Worker zum naechsten weitergereicht werden, etwa fuer Dekodierung, Filterung und anschliessende Kodierung. Ohne Transfer wuerde bei jedem Schritt eine vollstaendige Kopie anfallen, was die Gesamtlaufzeit der Pipeline unnoetig verlaengert.

In Kombination mit OffscreenCanvas laesst sich sogar das Rendering selbst in einen Worker auslagern und ein fertig gerendertes ImageBitmap per Transfer zurueck an den Main Thread geben, wodurch aufwendige Bildverarbeitung ausserhalb des UI-Threads laeuft, ohne dass am Ende eine teure Kopie des Ergebnisses noetig ist.


// Main Thread
const offscreen = canvas.transferControlToOffscreen();
const renderWorker = new Worker("render-worker.js");
renderWorker.postMessage({ canvas: offscreen }, [offscreen]);

// render-worker.js
self.onmessage = (event) => {
  const ctx = event.data.canvas.getContext("2d");
  ctx.fillStyle = "blue";
  ctx.fillRect(0, 0, 100, 100); // Rendering laeuft im Worker
};

9. Grenzen und Empfehlung

Wichtig zu wissen: Nach einem Transfer ist der urspruengliche Buffer im sendenden Kontext nicht mehr nutzbar, was bedeutet, dass er dort nicht mehr fuer eigene Berechnungen zur Verfuegung steht. Wird der Buffer im Sender noch weiter gebraucht, muss stattdessen eine echte Kopie erstellt und nur diese Kopie transferiert werden.

Als Empfehlung gilt: Fuer kleine, gelegentliche Nachrichten reicht Structured Clone vollkommen aus. Sobald wiederholt grosse Binaerdaten zwischen Threads bewegt werden, etwa in Streaming- oder Verarbeitungs-Pipelines, sollte konsequent auf Transferable Objects gesetzt werden, kombiniert mit klarer Verantwortung dafuer, welcher Thread jeweils Besitzer der Daten ist. Ein knapper Kommentar an jeder Transfer-Stelle im Code, der die neue Besitzverhaeltnis-Erwartung dokumentiert, erspart spaeter viel Zeit bei der Fehlersuche.

Objekt / API Transferable? Verhalten nach Transfer Typischer Einsatz
ArrayBuffer Ja byteLength wird 0, detached Binaerdaten, Sensor- und Mediendaten
MessagePort Ja Port im Sender nicht mehr nutzbar Direkte Kommunikationskanaele zwischen Workern
ImageBitmap Ja Bitmap im Sender nicht mehr nutzbar Bilddaten zwischen Worker und Main Thread
OffscreenCanvas Ja Canvas-Steuerung geht auf Empfaenger ueber Rendering ausserhalb des UI-Threads
TypedArray View Nein, nur .buffer ist transferable View selbst wird nicht transferiert Zugriff auf ArrayBuffer-Inhalte

Mironsoft

Moderne Browser-APIs, Performance und wartbares JavaScript

JavaScript, das im echten Browser robust bleibt, nicht nur im Tutorial?

Wir prüfen bestehenden Frontend-Code auf veraltete Patterns, unnötige Bibliotheken und Performance-Fallen und ersetzen sie durch moderne, native Browser-APIs, die weniger Bundle-Gewicht und weniger Wartungslast bedeuten.

Code-Review

Veraltete Patterns, unnötige Dependencies und Memory Leaks systematisch aufspüren.

Performance-Optimierung

Bundle-Größe, Ladezeit und Runtime-Performance mit modernen APIs verbessern.

Modernisierung

Native Browser-APIs statt schwerer Bibliotheken gezielt einführen.

10. Zusammenfassung

Transferable Objects: Das Wichtigste auf einen Blick

Structured Clone

Standardverhalten von postMessage, kopiert Daten vollstaendig, Kosten skalieren mit Groesse

Transfer-Liste

Zweiter Parameter von postMessage, uebertraegt Ownership statt zu kopieren

ArrayBuffer.transfer()

ES2024-Methode fuer expliziten Transfer auch innerhalb desselben Threads

Detached Buffer

byteLength wird 0, jede weitere Nutzung im Sender ist ausgeschlossen

11. FAQ: Transferable Objects: Das Wichtigste auf einen Blick

1Was ist ein Transferable Object in JavaScript?
Ein Transferable Object ist ein Objekt wie ArrayBuffer, MessagePort, ImageBitmap oder OffscreenCanvas, dessen zugrundeliegender Speicher bei postMessage nicht kopiert, sondern per Ownership-Wechsel an den Empfaenger uebergeben werden kann.
2Wie unterscheidet sich Transfer von Structured Clone?
Structured Clone kopiert die Daten vollstaendig, wodurch Sender und Empfaenger unabhaengige Kopien besitzen. Transfer uebertraegt lediglich die Ownership desselben Speicherbereichs, ohne die Daten zu kopieren.
3Wie nutze ich die Transfer-Liste bei postMessage?
Die Transfer-Liste wird als zweiter Parameter von postMessage uebergeben, ein Array aus den zu transferierenden Objekten, zum Beispiel worker.postMessage({ buffer }, [buffer]).
4Was passiert mit einem ArrayBuffer nach dem Transfer?
Der urspruengliche ArrayBuffer wird detached, seine byteLength wird 0 und er kann im sendenden Kontext nicht mehr fuer Lese- oder Schreibzugriffe genutzt werden.
5Was macht ArrayBuffer.prototype.transfer()?
Diese seit ES2024 verfuegbare Methode erzeugt einen neuen ArrayBuffer, der den Inhalt des urspruenglichen Buffers uebernimmt, waehrend der urspruengliche Buffer sofort detached wird, auch innerhalb desselben Threads.
6Sind TypedArrays wie Uint8Array direkt transferable?
Nein, TypedArrays selbst sind nicht transferable, aber ihr zugrundeliegendes .buffer ist es. Bei einem TypedArray muss deshalb array.buffer in die Transfer-Liste aufgenommen werden.
7Ab welcher Datengroesse lohnt sich Transfer gegenueber Kopie?
Bei kleinen Datenmengen im Kilobyte-Bereich ist der Unterschied kaum messbar. Ab einigen Megabyte, etwa bei Bild- oder Audiodaten, wird der Performance-Gewinn deutlich spuerbar.
8Was ist ein haeufiger Fehler im Umgang mit Transferable Objects?
Der haeufigste Fehler ist der Zugriff auf eine alte Referenz nach dem Transfer, was keinen Laufzeitfehler wirft, aber stillschweigend leere oder unerwartete Daten liefert.
9Kann ich einen bereits detachten Buffer erneut transferieren?
Nein, ein bereits detachter Buffer hat eine byteLength von 0 und enthaelt keine Daten mehr, ein erneuter Transfer ergibt keinen Sinn und sollte vorher explizit geprueft werden.
10Wofuer eignet sich OffscreenCanvas in Kombination mit Transfer?
OffscreenCanvas erlaubt, Rendering-Arbeit in einen Web Worker auszulagern und die Canvas-Steuerung per transferControlToOffscreen und Transfer-Liste an den Worker zu uebergeben, ohne den UI-Thread zu blockieren.