TensorFlow.jsからLiteRT.jsへ乗り換える際に直面する3つの地雷原
TuBrief Editorial
July 18, 2026
0
Computing/SoftwareWritten with AI assistance from the source video. The video is the authority.
More from the community
Comments (0)
Log in to leave a comment
No posts yet
Written with AI assistance from the source video. The video is the authority.
Log in to leave a comment
No posts yet
WebブラウザでAIモデルを動かすのは刺激的ですが、すぐに限界に直面します。高解像度の画像を処理したり重い演算を開始したりすると、画面がカクついてしまうからです。TensorFlow.js (TF.js) はWebGLバックエンドとJavaScriptカーネルバインディングを使用しますが、これが大規模な行列演算において深刻なオーバーヘッドを引き起こします。
一方、LiteRT.jsはC++ネイティブランタイムをWebAssembly (Wasm) にコンパイルしてブラウザに移植しました。ボトルネックを根本から解決する構造です。さらに、PyTorchモデルをWebに持ち込むプロセスも簡素化されます。以前は、PyTorchからONNXへ、そしてTensorFlowを経由してTF.jsフォーマットに変換する過程で、演算子の互換性が損なわれ、精度も低下していました。今ではライブラリ(ai-edge-torch)一つで標準の .tflite ファイルを出力すれば完了です。
既存の資産を捨てるのが惜しい場合は、Googleが提供する @litertjs/tfjs-interop パッケージを使えばよいでしょう。データの前処理や後処理はTF.jsのパイプラインをそのまま維持し、コアとなるモデル予測実行部のみをLiteRT.jsに置き換える方式です。
PyTorchから変換したTFLiteモデルは、通常チャンネル優先構造である(チャンネル、高さ、幅)を要求します。しかし、ブラウザのCanvasにおけるImageData配列はピクセルが並んだ(高さ、幅、チャンネル)フォーマットです。このギャップをメインスレッドに負荷をかけずに同期処理する前処理コードが必要です。
まず、ImageDataの全ピクセル数に3を掛けてFloat32Arrayを作成します。次に、赤、緑、青の各チャンネルの平坦化オフセット位置を0、全ピクセル数、全ピクセル数の2倍に指定します。最後に、0〜255のピクセル値を255.0で割って正規化した後、各チャンネルのオフセットに割り当てます。これは入力データを平坦化テンソルバッファへと並び替えるプロセスです。
`javascript
/**
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;
}
`
フロントエンドでディープラーニングを駆動する際、最も恐ろしい状況はUIがフリーズすることです。ブラウザが毎秒60フレームの滑らかなアニメーションを表示するには、イベントループが16.6ms以内に同期処理を完了させる必要があります。しかし、テンソル演算はシングルスレッドのメインレンダラーループを容易にブロックしてしまいます。
LiteRT.jsは従来のJavaScriptベースのツールよりも基本実行速度が約3倍速いです。WebGPUやWebNNアクセラレーションハードウェアを接続すれば、CPUモードより最低5倍から最大60倍まで高速化します。専用NPUを使用するWebNNバックエンドは、同期式WebAssemblyカーネルスケジューラとブラウザの非同期ハードウェア制御ループを接続するために、JavaScriptプロミス統合(JSPI)機能を必ず有効にする必要があります。これらの加速リソースを使用しつつメインスレッドを保護するには、ライブラリの初期化と推論パイプライン全体をWeb Worker内部に隔離するのが正解です。
この際、スレッド間のデータ通信でバッファをそのまま渡すと内部メモリコピーが発生し、CPUとヒープメモリにオーバーヘッドが蓄積します。転送可能オブジェクト (Transferable Objects) を使用して物理メモリのアドレス領域の所有権そのものを移譲しなければ、遅延は解消されません。所有権が移転されたバッファは、送信側のコンテキストですぐに無効化されるため、スレッド間の安全性も担保できます。
`javascript
// litert-worker.js - バックグラウンド推論演算専用Web Workerモジュール
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 - メインスレッド用AIオーケストレータークラス
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]
);
});
}
}
`
TF.jsを使用する際は tf.tidy() で同期呼び出しスコープ内のテンソルを破棄するパターンが標準でした。しかし、非同期コードやPromiseが絡むと、非同期処理が終わる前にスコープを抜けてテンソルを破壊してしまったり、収集を漏らしたりするバグを生んでいました。
LiteRT.jsはさらに厳格です。ブラウザエンジンのガベージコレクション (GC) の対象ではありません。WebAssemblyの線形仮想メモリ空間とWebGPUバッファは、V8のようなJavaScriptエンジンがヒープ状態を追跡できません。コードが終了したテンソルインスタンスに対して明示的に .delete() を呼び出さなければ、ブラウザメモリは無限に蓄積されます。高画質ビデオフレームを毎秒数十回ストリーミングするようなサービスであれば、数分以内にタブがクラッシュしてしまいます。
非同期パイプライン全体で生成されたテンソルの寿命を記録し、一括破棄を保証するスコープトラッカークラスを作成して手動で管理するのが安心です。
`javascript
/**
/**
/**
/**
`
`javascript
// メモリスコーピントラッカーを活用した安全かつ堅牢な多重非同期AI推論処理の実装例
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;
}
}
`
リソースのダウンロードサイズが大きくなる問題も無視できません。ビルドバンドラーでツリーシェイキングを機能させるには、CommonJSスタイルの静的参照を削除し、ES6モジュール構文 (import/export) ベースでソースコードを記述する必要があります。ビルドツールの sideEffects: false 設定も確認して、バンドルを軽量化しましょう。
LiteRT.jsのコアランタイムである @litertjs/core は、機器の性能に合わせて3つのWebAssemblyカーネルビルドを選択してロードします。ChromeやEdgeのような最新ブラウザではマルチスレッドとSIMDをサポートするモジュール (litert_wasm_simd.wasm) を選択し、Safariのようなレガシー環境ではデフォルトのフォールバックモジュール (litert_wasm.wasm) を取得します。もしGPUコンパイルが失敗した場合は、ハードウェア演算子全体をCPU Wasmサンドボックスへ逃がすXNNPACKランタイムが補助装置として動作します。
初期ロードの遅延を防ぐには、仕様ごとにアクセラレータをチェックし、動的にモジュールを取得しなければなりません。
`javascript
// litert-loader.js - ランタイム機器探索およびアクセラレータ動的結合エンジン
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
};
}
`
Web Workerを通じたメモリ所有権の移譲と、Wasm領域の明示的なオブジェクト消滅処理を設計の基盤に据えましょう。データフローを直接制御し始めれば、ブラウザのクラッシュを恐れることなく、オンデバイスAIサービスを本番環境へデプロイできます。