TuBrief
Subscribed Channels
Videos
Community

Drei Minenfelder beim Wechsel von TensorFlow.js zu LiteRT.js

TuBrief Editorial
July 18, 2026
0
Computing/Software

Written with AI assistance from the source video. The video is the authority.

Deutsch한국어EnglishEspañol中文العربيةहिन्दीFrançaisPortuguêsРусскийBahasa Indonesia日本語

Related Video

Google hat TensorFlow.js gerade überflüssig gemacht (LiteRT.js)8:10

Google hat TensorFlow.js gerade überflüssig gemacht (LiteRT.js)

Better Stack

More from the community

사내 시스템에 llm api 붙일 때 마주하는 현실적인 한계와 대응법

September 13, 2026

레거시 백엔드에 GPT-6 Astra 붙일 때 예산 승인과 보안 통과를 먼저 끝내는 법이 있습니다

September 13, 2026

에이전트끼리 대화하다 6천만 원 청구서가 나오는 이유

September 13, 2026

사내 RAG 벡터 검색에 Okta 권한 필터를 직접 거는 방법

September 13, 2026

브라우저 에이전트에게 내 구글 계정을 통째로 넘기면 안 되는 이유

September 12, 2026

Apple Won the AI Race

September 12, 2026

Comments (0)

Log in to leave a comment

No posts yet

© 2026 . All rights reserved.

TuBrief
Subscribed Channels
Videos
Community
Log in

Drei Minenfelder beim Wechsel von TensorFlow.js zu LiteRT.js

Das Ausführen von KI-Modellen im Webbrowser ist zwar aufregend, stößt aber schnell an Grenzen. Sobald hochauflösende Bilder verarbeitet oder rechenintensive Operationen gestartet werden, beginnt das Bild zu ruckeln. TensorFlow.js (TF.js) nutzt das WebGL-Backend und JavaScript-Kernel-Bindings, was bei umfangreichen Matrixoperationen zu erheblichem Overhead führt.

LiteRT.js hingegen portiert die native C++-Runtime via WebAssembly (Wasm) in den Browser. Dies ist eine Struktur, die Engpässe grundlegend löst. Zudem wird der Prozess, PyTorch-Modelle in das Web zu bringen, vereinfacht. Früher musste man von PyTorch zu ONNX und dann über TensorFlow in das TF.js-Format konvertieren, was oft zu Problemen bei der Operator-Kompatibilität und Präzisionsverlusten führte. Heute genügt eine einzelne Bibliothek (ai-edge-torch), um eine standardisierte .tflite-Datei zu erstellen.

Wer seine bestehenden Ressourcen nicht aufgeben möchte, kann das von Google bereitgestellte Paket @litertjs/tfjs-interop verwenden. Dabei werden die Datenvor- oder -nachverarbeitung in der TF.js-Pipeline belassen, während nur der Kernbereich der Modellausführung durch LiteRT.js ersetzt wird.

Behebung der Diskrepanz zwischen NCHWNCHWNCHW und NHWCNHWCNHWC

Ein aus PyTorch konvertiertes TFLite-Modell erfordert meist eine kanalbasierte Struktur namens NCHWNCHWNCHW (Channel, Height, Width). Das ImageData-Array eines Browser-Canvas ist jedoch im NHWCNHWCNHWC-Format (Height, Width, Channel) angeordnet, in dem die Pixel nacheinander vorliegen. Es wird ein Vorverarbeitungscode benötigt, der diese Lücke synchron ohne Belastung des Main-Threads schließt.

Zuerst erstellt man ein Float32Array, indem die Gesamtzahl der Pixel mit 3 multipliziert wird. Danach legt man die Offsets für die abgeflachten Rot-, Grün- und Blaukanäle auf 0, die Gesamtzahl der Pixel bzw. das Doppelte der Gesamtzahl fest. Schließlich werden die Pixelwerte von 0-255 durch 255.0 normalisiert und den jeweiligen Kanal-Offsets zugewiesen. Dies ist der Prozess der Umordnung von Eingabedaten in einen abgeflachten NCHWNCHWNCHW-Tensor-Puffer.

`javascript
/**

  • Vorverarbeitungs-Utility zur Hochgeschwindigkeitskonvertierung von NHWC-ImageData-Puffern in leistungsstarke NCHW Float32Arrays
  • @param {ImageData} imageData - Original-Pixeldaten vom HTML5 Canvas
  • @param {number} width - Die vom Zielmodell geforderte Eingabebildbreite
  • @param {number} height - Die vom Zielmodell geforderte Eingabebildhöhe
  • @returns {Float32Array} Als NCHW-Array neu angeordneter, abgeflachter Tensor-Puffer
    */
    export function preprocessNHWCToNCHW(imageData, width, height) {
    const { data } = imageData;
    const totalPixels = width * height;
    const nchwBuffer = new Float32Array(totalPixels * 3);

const rChannelOffset = 0;
const gChannelOffset = totalPixels;
const bChannelOffset = totalPixels * 2;

for (let i = 0; i < totalPixels; i++) {
const srcIndex = i * 4;
nchwBuffer[rChannelOffset + i] = data[srcIndex] / 255.0;
nchwBuffer[gChannelOffset + i] = data[srcIndex + 1] / 255.0;
nchwBuffer[bChannelOffset + i] = data[srcIndex + 2] / 255.0;
}
return nchwBuffer;
}

`

Ruckeln verhindern mit Web Workern und Zero-Copy

Das schlimmste Szenario bei der Ausführung von Deep Learning im Frontend ist ein einfrierendes UI. Damit der Browser flüssige Animationen mit 60 Bildern pro Sekunde anzeigen kann, muss die Event-Loop synchrone Aufgaben innerhalb von 16,6 ms verarbeiten. Tensor-Operationen blockieren jedoch leicht den Single-Threaded Main-Renderer-Loop.

LiteRT.js bietet eine um etwa dreimal schnellere Basis-Ausführungsgeschwindigkeit als bisherige JavaScript-basierte Tools. Mit WebGPU- oder WebNN-Beschleunigungshardware steigt die Geschwindigkeit gegenüber dem CPU-Modus um das 5- bis 60-fache. Das WebNN-Backend, das eine dedizierte NPU nutzt, erfordert zwingend die Aktivierung der JavaScript Promise Integration (JSPI), um den synchronen WebAssembly-Kernel-Scheduler mit der asynchronen Hardware-Steuerungsschleife des Browsers zu verbinden. Um diese Beschleunigungsressourcen zu nutzen und gleichzeitig den Main-Thread zu schonen, sollten die Bibliotheksinitialisierung sowie die gesamte Inferenz-Pipeline zwingend innerhalb eines Web Workers isoliert werden.

Bei der Datenkommunikation zwischen Threads führt das einfache Übergeben von Puffern zu internen Speicherkopien, was Overhead in der CPU und im Heap-Speicher erzeugt. Durch die Verwendung von Transferable Objects wird das Eigentumsrecht am physischen Speicheradressbereich direkt übertragen, wodurch die Latenz verschwindet. Da die übertragenen Puffer im Kontext der Senderseite sofort invalidiert werden, bleibt die Sicherheit zwischen den Threads gewahrt.

`javascript
// litert-worker.js - Web Worker Modul für Hintergrund-Inferenzberechnungen
import { loadLiteRt, loadAndCompile, Tensor } from '@litertjs/core';

let compiledModel = null;
let isLoaded = false;

self.onmessage = async (event) => {
const { type, payload } = event.data;

switch (type) {
case 'LOAD_MODEL':
try {
await loadLiteRt(payload.wasmDirectory, { jspi: payload.enableJspi || false });
compiledModel = await loadAndCompile(payload.modelUrl, {
accelerator: payload.accelerator || 'webgpu'
});
isLoaded = true;
self.postMessage({ type: 'MODEL_READY' });
} catch (err) {
self.postMessage({ type: 'ERROR', error: Initialization failed: ${err.message} });
}
break;

case 'RUN_INFERENCE':
  if (!isLoaded || !compiledModel) {
    self.postMessage({ type: 'ERROR', error: 'Model has not been loaded' });
    return;
  }
  try {
    const rawInputData = payload.bufferData;
    const inputShape = payload.shape;
    const inputTensor = new Tensor(rawInputData, inputShape);
    const results = await compiledModel.run(inputTensor);
    const cpuOutputTensor = await results[0].moveTo('wasm');
    const outputBuffer = cpuOutputTensor.toTypedArray();

    inputTensor.delete();
    cpuOutputTensor.delete();
    results[0].delete();

    self.postMessage(
      {
        type: 'INFERENCE_COMPLETE',
        payload: {
          data: outputBuffer,
          shape: results[0].shape
        }
      },
      [outputBuffer.buffer]
    );
  } catch (err) {
    self.postMessage({ type: 'ERROR', error: `Inference failed: ${err.message}` });
  }
  break;

default:
  self.postMessage({ type: 'UNKNOWN_OP' });

}
};

`

`javascript
// litert-bridge.js - KI-Orchestrator-Klasse für den Main-Thread
export class LiteRtBridge {
constructor(workerPath) {
this.worker = new Worker(workerPath);
this.promiseMap = new Map();
this.tokenCounter = 0;

this.worker.onmessage = (event) => {
  const { type, payload, error } = event.data;

  if (type === 'MODEL_READY') {
    if (this.initResolve) this.initResolve();
  } else if (type === 'INFERENCE_COMPLETE') {
    const currentToken = this.tokenCounter;
    const promiseHandler = this.promiseMap.get(currentToken);
    if (promiseHandler) {
      promiseHandler.resolve(payload);
      this.promiseMap.delete(currentToken);
    }
  } else if (type === 'ERROR') {
    const currentToken = this.tokenCounter;
    const promiseHandler = this.promiseMap.get(currentToken);
    if (promiseHandler) {
      promiseHandler.reject(new Error(error));
      this.promiseMap.delete(currentToken);
    } else if (this.initReject) {
      this.initReject(new Error(error));
    }
  }
};

}

bootstrap(wasmDirectory, modelUrl, accelerator = 'webgpu') {
return new Promise((resolve, reject) => {
this.initResolve = resolve;
this.initReject = reject;
this.worker.postMessage({
type: 'LOAD_MODEL',
payload: { wasmDirectory, modelUrl, accelerator, enableJspi: true }
});
});
}

execute(inputFloat32Array, inputShape) {
return new Promise((resolve, reject) => {
this.tokenCounter++;
this.promiseMap.set(this.tokenCounter, { resolve, reject });
this.worker.postMessage(
{
type: 'RUN_INFERENCE',
payload: {
bufferData: inputFloat32Array,
shape: inputShape
}
},
[inputFloat32Array.buffer]
);
});
}
}

`

Vertraue nicht der automatischen Garbage Collection

Bei der Verwendung von TF.js war das Muster, Tensoren innerhalb eines synchronen Aufruf-Scopes mit tf.tidy() zu bereinigen, Standard. Verstrickten sich jedoch asynchroner Code oder Promises, entstanden oft Bugs, bei denen Tensoren zu früh zerstört oder gar nicht erst aufgeräumt wurden.

LiteRT.js ist hier unerbittlicher. Es unterliegt nicht der Garbage Collection (GC) der Browser-Engine. Der lineare virtuelle Speicherbereich von WebAssembly und WebGPU-Puffer können von JavaScript-Engines wie V8 nicht in ihrem Heap-Status nachverfolgt werden. Wenn man nicht explizit .delete() auf Tensor-Instanzen aufruft, deren Lebensdauer abgelaufen ist, wächst der Browserspeicher ins Unendliche. Bei einem Dienst, der hochauflösende Video-Frames dutzende Male pro Sekunde streamt, stürzt der Tab innerhalb weniger Minuten ab.

Um Sicherheit zu gewinnen, sollte man eine Scope-Tracker-Klasse implementieren, die die Lebensdauer der über die gesamte asynchrone Pipeline erzeugten Tensoren protokolliert und eine stapelweise Zerstörung garantiert.

`javascript
/**

  • Asynchroner Memory-Scope-Manager, der das manuelle Tracking und die sichere Zerstörung von WebAssembly-Heap-Tensoren vermittelt
    */
    export class LiteRtScopeTracker {
    constructor() {
    this.trackList = new Set();
    }

/**

  • Überträgt einen erzeugten oder verschobenen Tensor in die Lifecycle-Verwaltungsliste
  • @param {Tensor} tensor - LiteRT.js Tensor, dessen Nachverfolgung und Zerstörung verwaltet werden soll
  • @returns {Tensor} Gibt das Tensor-Objekt unverändert zurück, um das Schreiben von Inline-Code zu unterstützen
    */
    register(tensor) {
    if (tensor && typeof tensor.delete === 'function') {
    this.trackList.add(tensor);
    }
    return tensor;
    }

/**

  • Erzwingt eine sichere Tensor-Pipeline-Verwaltungsstruktur innerhalb asynchroner Ausführungsblöcke
  • @param {Function} asyncCallable - Asynchrone Inferenz-Business-Logik-Funktion
  • @returns {Promise<*>} Das endgültige Rohdatenergebnis, das vom Ausführungsblock zurückgegeben wird
    */
    async enforceScope(asyncCallable) {
    try {
    const outputResult = await asyncCallable(this);
    if (Array.isArray(outputResult)) {
    outputResult.forEach((item) => this.trackList.delete(item));
    } else {
    this.trackList.delete(outputResult);
    }
    return outputResult;
    } finally {
    this.disposeAll();
    }
    }

/**

  • Entfernt die Bindung aller in der Wasm-Region überlebenden nativen TFLite-Tensoren aus der Verwaltung
    */
    disposeAll() {
    for (const tensor of this.trackList) {
    try {
    tensor.delete();
    } catch (err) {
    console.error('An error occurred while cleaning the native Wasm tensor memory:', err);
    }
    }
    this.trackList.clear();
    }
    }

`

`javascript
// Beispiel für die Implementierung einer sicheren und robusten Multi-Asynchron-KI-Inferenz unter Verwendung des Memory-Scope-Trackers
export async function runRobustVisionInference(rawPixelArray, compiledModel) {
const scopeTracker = new LiteRtScopeTracker();
try {
return await scopeTracker.enforceScope(async (scope) => {
const inputTensor = scope.register(new Tensor(rawPixelArray, [1, 3, 224, 224]));
const predictionResults = await compiledModel.run(inputTensor);
predictionResults.forEach((tensor) => scope.register(tensor));

  const firstOutputTensor = predictionResults[0];
  const wasmTransferTensor = scope.register(await firstOutputTensor.moveTo('wasm'));
  const targetJsArray = wasmTransferTensor.toTypedArray();

  return targetJsArray;
});

} catch (err) {
console.error('Fatal crash occurred during the model pipeline execution:', err);
throw err;
}
}

`

Bedingtes Laden großer Wasm-Dateien

Das Problem der wachsenden Downloadgröße der Ressourcen ist ebenfalls nicht zu vernachlässigen. Damit Tree-Shaking im Build-Bundler funktioniert, sollte man CommonJS-Stil-Referenzen entfernen und den Quellcode auf Basis der ES6-Modulsyntax (import/export) schreiben. Auch die sideEffects: false-Einstellung des Build-Tools sollte beachtet werden, um das Bundle schlank zu halten.

Die LiteRT.js Core-Runtime @litertjs/core lädt je nach Geräte-Performance einen von drei WebAssembly-Kernel-Builds. In modernen Browsern wie Chrome oder Edge wird ein Modul geladen, das Multithreading und SIMD unterstützt (litert_wasm_simd.wasm), während in Legacy-Umgebungen wie Safari auf das Standard-Fallback-Modul (litert_wasm.wasm) zurückgegriffen wird. Sollte die GPU-Kompilierung fehlschlagen, agiert die XNNPACK-Runtime als Unterstützung, die alle Hardware-Operatoren in die CPU-Wasm-Sandbox verlagert.

Um Verzögerungen beim initialen Laden zu vermeiden, sollten Module dynamisch basierend auf der Prüfung der verfügbaren Beschleuniger geladen werden.

`javascript
// litert-loader.js - Engine zur Runtime-Geräteerkennung und dynamischen Einbindung von Beschleunigern
export async function bootstrapHighPerformanceInferenceEngine() {
const supportsWebGpu = 'gpu' in navigator;
let chosenAccelerator = 'wasm';

if (supportsWebGpu) {
try {
const gpuAdapter = await navigator.gpu.requestAdapter();
if (gpuAdapter) {
const info = await gpuAdapter.requestDevice();
if (info) {
chosenAccelerator = 'webgpu';
}
}
} catch (e) {
console.warn("GPU profile probe failed, resolving execution chain to fallback WASM.");
}
}

const { loadLiteRt, loadAndCompile } = await import('@litertjs/core');
const cdnWasmHostPath = 'https://cdn.jsdelivr.net/npm/@litertjs/core/wasm/';

await loadLiteRt(cdnWasmHostPath, {
jspi: chosenAccelerator === 'webnn'
});

return {
loadAndCompile,
chosenAccelerator
};
}

`

Lassen Sie die Übertragung des Speicherbesitzes über Web Worker und die explizite Zerstörung von Objekten im Wasm-Bereich zum Grundpfeiler Ihres Designs werden. Sobald Sie beginnen, den Datenfluss direkt zu steuern, können Sie On-Device-KI-Dienste in Produktion bringen, ohne sich um Browser-Abstürze sorgen zu müssen.