TuBrief
Subscribed Channels
Videos
Community

TensorFlow.jsからLiteRT.jsへ乗り換える際に直面する3つの地雷原

TuBrief Editorial
July 18, 2026
0
Computing/Software

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

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

Related Video

GoogleがTensorFlow.jsを事実上終了 (LiteRT.jsの登場)8:10

GoogleがTensorFlow.jsを事実上終了 (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

TensorFlow.jsからLiteRT.jsへ乗り換える際に直面する3つの地雷原

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に置き換える方式です。

NCHWNCHWNCHWとNHWCNHWCNHWCの不一致を解決する

PyTorchから変換したTFLiteモデルは、通常チャンネル優先構造であるNCHWNCHWNCHW(チャンネル、高さ、幅)を要求します。しかし、ブラウザのCanvasにおけるImageData配列はピクセルが並んだNHWCNHWCNHWC(高さ、幅、チャンネル)フォーマットです。このギャップをメインスレッドに負荷をかけずに同期処理する前処理コードが必要です。

まず、ImageDataの全ピクセル数に3を掛けてFloat32Arrayを作成します。次に、赤、緑、青の各チャンネルの平坦化オフセット位置を0、全ピクセル数、全ピクセル数の2倍に指定します。最後に、0〜255のピクセル値を255.0で割って正規化した後、各チャンネルのオフセットに割り当てます。これは入力データをNCHWNCHWNCHW平坦化テンソルバッファへと並び替えるプロセスです。

`javascript
/**

  • NHWCフォーマットのImageDataバッファを高性能なNCHW Float32Arrayへ高速変換する前処理ユーティリティ
  • @param {ImageData} imageData - HTML5 Canvasから取得した元ピクセルデータ
  • @param {number} width - 対象モデルが要求する入力画像の横解像度
  • @param {number} height - 対象モデルが要求する入力画像の縦解像度
  • @returns {Float32Array} NCHW配列形式に並び替えられた平坦化テンソルバッファ
    */
    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;
}

`

Web Workerとゼロコピーで画面のカクつきを防ぐ

フロントエンドでディープラーニングを駆動する際、最も恐ろしい状況は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
/**

  • WebAssemblyヒープテンソルの手動追跡および確実な破棄を仲介する非同期メモリ範囲マネージャ
    */
    export class LiteRtScopeTracker {
    constructor() {
    this.trackList = new Set();
    }

/**

  • 生成または移動完了したテンソルをライフサイクル管理リストに編入
  • @param {Tensor} tensor - 追跡および消滅処理を収束させるLiteRT.jsテンソル
  • @returns {Tensor} 入手したテンソルオブジェクトをそのまま返し、インラインコードの記述をサポート
    */
    register(tensor) {
    if (tensor && typeof tensor.delete === 'function') {
    this.trackList.add(tensor);
    }
    return tensor;
    }

/**

  • 非同期実行ブロック内の安全なテンソルパイプライン管理構造の強制化
  • @param {Function} asyncCallable - 非同期推論ビジネスロジック関数
  • @returns {Promise<*>} 任意の実行ブロックが返す最終的な生データ結果
    */
    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();
    }
    }

/**

  • 管理対象にバインドされて生存しているすべてのネイティブTFLiteテンソルをWasm領域から永久に隔離解除
    */
    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
// メモリスコーピントラッカーを活用した安全かつ堅牢な多重非同期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;
}
}

`

重いWasmファイルを条件付きでロードする

リソースのダウンロードサイズが大きくなる問題も無視できません。ビルドバンドラーでツリーシェイキングを機能させるには、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サービスを本番環境へデプロイできます。