TuBrief
Subscribed Channels
Videos
Community

Tres campos de minas a sortear al pasar de TensorFlow.js a LiteRT.js

TuBrief Editorial
July 18, 2026
0
Computing/Software

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

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

Related Video

Google acaba de hacer obsoleto a TensorFlow.js (LiteRT.js)8:10

Google acaba de hacer obsoleto a 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

Tres campos de minas a sortear al pasar de TensorFlow.js a LiteRT.js

Ejecutar modelos de IA en el navegador web es emocionante, pero pronto te topas con limitaciones. Cuando procesas imágenes de alta resolución o inicias cálculos pesados, la pantalla comienza a sufrir tirones. TensorFlow.js (TF.js) utiliza el backend de WebGL y el binding de núcleos de JavaScript, lo cual genera una sobrecarga significativa en operaciones matriciales a gran escala.

Por el contrario, LiteRT.js trasplanta el runtime nativo de C++ al navegador mediante la compilación a WebAssembly (Wasm). Es una estructura que resuelve los cuellos de botella de raíz. Además, el proceso de llevar modelos de PyTorch a la web se simplifica. Antes, el flujo de PyTorch a ONNX, pasando por TensorFlow para llegar al formato TF.js, provocaba la ruptura de la compatibilidad de operadores y la pérdida de precisión. Ahora, con una sola librería (ai-edge-torch), basta con exportar un archivo estándar .tflite y listo.

Si no quieres desechar tus recursos existentes, puedes usar el paquete @litertjs/tfjs-interop proporcionado por Google. El enfoque consiste en mantener el preprocesamiento y postprocesamiento de datos en el pipeline de TF.js, sustituyendo únicamente la parte de ejecución de inferencia del modelo por LiteRT.js.

Resolviendo la discrepancia entre NCHWNCHWNCHW y NHWCNHWCNHWC

Los modelos TFLite convertidos desde PyTorch suelen requerir una estructura de canales primero (NCHWNCHWNCHW: canales, alto, ancho). Sin embargo, el array ImageData de un Canvas en el navegador sigue el formato NHWCNHWCNHWC (alto, ancho, canales), donde los píxeles están dispuestos de forma contigua. Se necesita un código de preprocesamiento que resuelva esta diferencia de forma síncrona y sin sobrecargar el hilo principal.

Primero, creamos un Float32Array multiplicando el número total de píxeles de ImageData por 3. Luego, definimos las posiciones de desplazamiento (offset) de aplanamiento para los canales rojo, verde y azul en 0, el total de píxeles y el doble del total de píxeles, respectivamente. Finalmente, normalizamos los valores de los píxeles de 0 a 255 dividiéndolos por 255.0 y los asignamos a cada desplazamiento de canal. Este es el proceso de reorganización de los datos de entrada en un búfer de tensores aplanados NCHWNCHWNCHW.

`javascript
/**

  • Utilidad de preprocesamiento para la conversión de alta velocidad de un búfer ImageData en formato NHWC a NCHW Float32Array
  • @param {ImageData} imageData - Datos de píxeles crudos obtenidos desde un Canvas HTML5
  • @param {number} width - Resolución horizontal de la imagen de entrada requerida por el modelo
  • @param {number} height - Resolución vertical de la imagen de entrada requerida por el modelo
  • @returns {Float32Array} Búfer de tensores aplanados reorganizado en formato 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;
}

`

Evitar tirones en pantalla con Web Workers y "Zero-copy"

Lo peor que puede pasar al ejecutar aprendizaje profundo en el frontend es que la interfaz se congele. Para que el navegador muestre animaciones fluidas a 60 FPS, el bucle de eventos debe procesar las tareas síncronas en menos de 16.6 ms. Sin embargo, las operaciones con tensores bloquean fácilmente el bucle del renderizador principal, que es monohilo.

LiteRT.js es aproximadamente tres veces más rápido en su ejecución base que las herramientas basadas en JavaScript tradicional. Al añadir aceleración mediante WebGPU o WebNN, la velocidad aumenta de 5 a 60 veces en comparación con el modo CPU. El backend de WebNN, que utiliza una NPU dedicada, requiere activar obligatoriamente la integración de promesas de JavaScript (JSPI) para conectar el planificador de núcleos de WebAssembly síncronos con el bucle de control de hardware asíncrono del navegador. Para aprovechar esta aceleración manteniendo el hilo principal activo, la inicialización de la librería y todo el pipeline de inferencia deben aislarse dentro de un Web Worker.

En este punto, si simplemente pasas el búfer en la comunicación entre hilos, se produce una copia interna de memoria, lo que genera sobrecarga en la CPU y la memoria heap. Es necesario utilizar Transferable Objects para transferir la propiedad misma de la dirección de memoria física y eliminar la latencia. Una vez transferida la propiedad, el búfer queda invalidado inmediatamente en el contexto emisor, garantizando así la seguridad entre hilos.

`javascript
// litert-worker.js - Módulo de Web Worker exclusivo para operaciones de inferencia en segundo plano
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 - Clase orquestadora de IA para el hilo principal
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]
);
});
}
}

`

No confíes en la recolección de basura automática

Al usar TF.js, el patrón estándar era eliminar los tensores dentro de un scope de llamada síncrona mediante tf.tidy(). Sin embargo, cuando se entrelazan códigos asíncronos o promesas, esto provocaba errores, como liberar tensores antes de que terminara la tarea asíncrona o dejar de liberar recursos (memory leaks).

LiteRT.js es más implacable; no está sujeto a la recolección de basura (GC) del motor del navegador. El motor de JavaScript, como V8, no puede rastrear el estado del heap del espacio de memoria virtual lineal de WebAssembly ni de los búferes de WebGPU. Si no llamas explícitamente a .delete() en las instancias de tensores que ya no se usan, la memoria del navegador crecerá infinitamente. En un servicio que transmite fotogramas de video de alta calidad docenas de veces por segundo, la pestaña morirá en cuestión de minutos.

Para trabajar con tranquilidad, debes crear una clase rastreadora de scope que registre el tiempo de vida de los tensores generados en todo el pipeline asíncrono y garantice su destrucción colectiva.

`javascript
/**

  • Gestor asíncrono de ámbito de memoria que intermedia en el seguimiento manual y la destrucción segura de tensores en el heap de WebAssembly
    */
    export class LiteRtScopeTracker {
    constructor() {
    this.trackList = new Set();
    }

/**

  • Incorpora al registro de gestión de ciclo de vida un tensor creado o movido
  • @param {Tensor} tensor - Tensor de LiteRT.js que se consolidará para seguimiento y destrucción
  • @returns {Tensor} Retorna el objeto tensor recibido para facilitar la escritura de código en línea
    */
    register(tensor) {
    if (tensor && typeof tensor.delete === 'function') {
    this.trackList.add(tensor);
    }
    return tensor;
    }

/**

  • Fuerza una estructura de gestión de pipeline de tensores segura dentro de un bloque de ejecución asíncrono
  • @param {Function} asyncCallable - Función de lógica de negocio de inferencia asíncrona
  • @returns {Promise<*>} El resultado final de los datos crudos devuelto por un bloque de ejecución arbitrario
    */
    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();
    }
    }

/**

  • Libera permanentemente del espacio Wasm todos los tensores TFLite nativos que siguen activos vinculados a los objetos gestionados
    */
    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
// Ejemplo de implementación de procesamiento de inferencia de IA asíncrona múltiple, segura y robusta utilizando el rastreador de ámbito de memoria
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;
}
}

`

Carga condicional de archivos Wasm pesados

El problema del aumento del tamaño de la descarga de recursos tampoco debe ignorarse. Para que el tree-shaking funcione en el bundler de compilación, se debe eliminar la referencia estática al estilo CommonJS y escribir el código fuente basado en la sintaxis de módulos ES6 (import/export). También hay que asegurarse de incluir la configuración sideEffects: false de la herramienta de construcción para reducir el paquete final.

El runtime principal de LiteRT.js, @litertjs/core, selecciona dinámicamente tres builds de núcleos de WebAssembly según el rendimiento del dispositivo. En navegadores modernos como Chrome o Edge, se carga el módulo compatible con multihilo y SIMD (litert_wasm_simd.wasm), mientras que en entornos heredados como Safari, se utiliza el módulo de respaldo predeterminado (litert_wasm.wasm). Si la compilación de GPU falla, el runtime de XNNPACK entra como auxiliar para trasladar todos los operadores de hardware a una zona de pruebas (sandbox) de Wasm en la CPU.

Para evitar retrasos en la carga inicial, es fundamental verificar el acelerador según las especificaciones y cargar el módulo de forma dinámica.

`javascript
// litert-loader.js - Motor de exploración de dispositivos en tiempo de ejecución y acoplamiento dinámico de aceleradores
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
};
}

`

Establece como base de diseño la transferencia de propiedad de memoria a través de Web Workers y la destrucción explícita de objetos en el área Wasm. Una vez que empieces a controlar directamente el flujo de datos, podrás poner en producción servicios de IA en el dispositivo (on-device AI) sin temor a que el navegador explote.