TensorFlow.js: Machine Learning Directamente en el Browser Con JavaScript
Hola HaWkers, ¿y si pudieras ejecutar modelos de machine learning directamente en el navegador del usuario, sin precisar servidor? En 2025, esto no es más futuro - es realidad, y TensorFlow.js es la herramienta que torna esto posible.
En esta guía, vamos a explorar cómo usar TensorFlow.js para crear aplicaciones inteligentes que corren 100% en el client-side.
Por Qué Machine Learning en el Browser
Ejecutar ML en el browser ofrece ventajas únicas que muchos desarrolladores aún no explotan.
Beneficios
Privacidad:
- Datos nunca salen del dispositivo del usuario
- Sin preocupaciones con LGPD/GDPR para procesamiento
- Ideal para aplicaciones sensibles
Performance:
- Sin latencia de red para inferencia
- Usa GPU del dispositivo via WebGL
- Respuestas en tiempo real
Costo:
- Cero costo de servidor para inferencia
- Escala ilimitada sin aumentar infraestructura
- Democratiza acceso a ML
Experiencia:
- Funciona offline
- Interactividad en tiempo real
- No depende de conexión
🧠 Contexto: TensorFlow.js es usado por millones de desarrolladores y empresas como Google, Spotify y Airbnb para ML en el browser.
Primeros Pasos Con TensorFlow.js
Vamos a configurar un proyecto y entender los conceptos básicos.
Instalación
# Via NPM
npm install @tensorflow/tfjs
# O via CDN en el HTML
# <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>Conceptos Fundamentales
import * as tf from '@tensorflow/tfjs';
// Tensors: Arrays multidimensionales
const tensor1D = tf.tensor([1, 2, 3, 4]);
const tensor2D = tf.tensor([[1, 2], [3, 4]]);
// Shapes: Dimensiones del tensor
console.log(tensor2D.shape); // [2, 2]
// Operaciones básicas
const a = tf.tensor([1, 2, 3]);
const b = tf.tensor([4, 5, 6]);
const sum = a.add(b); // [5, 7, 9]
const product = a.mul(b); // [4, 10, 18]
const mean = a.mean(); // 2
// IMPORTANTE: Limpiar memoria
tensor1D.dispose();
tensor2D.dispose();
// O usar tf.tidy() para limpieza automáticaGerenciamiento de Memoria
// tf.tidy() limpia tensors intermediarios automáticamente
const result = tf.tidy(() => {
const a = tf.tensor([1, 2, 3]);
const b = tf.tensor([4, 5, 6]);
const c = a.add(b);
const d = c.mul(tf.scalar(2));
return d; // Apenas d es mantenido
});
// Verificar uso de memoria
console.log(tf.memory());
// { numTensors: X, numDataBuffers: Y, numBytes: Z }
Usando Modelos Pre-Entrenados
La forma más rápida de comenzar es usar modelos ya entrenados.
Clasificación de Imágenes Con MobileNet
import * as tf from '@tensorflow/tfjs';
import * as mobilenet from '@tensorflow-models/mobilenet';
async function classifyImage() {
// Carga el modelo (primera vez puede demorar)
const model = await mobilenet.load();
// Elemento de imagen del DOM
const img = document.getElementById('myImage');
// Clasifica la imagen
const predictions = await model.classify(img);
console.log(predictions);
// [
// { className: 'golden retriever', probability: 0.89 },
// { className: 'Labrador retriever', probability: 0.08 },
// { className: 'cocker spaniel', probability: 0.02 }
// ]
}Detección de Objetos Con COCO-SSD
import * as cocoSsd from '@tensorflow-models/coco-ssd';
async function detectObjects() {
const model = await cocoSsd.load();
const img = document.getElementById('myImage');
const predictions = await model.detect(img);
predictions.forEach(prediction => {
console.log(prediction);
// {
// bbox: [x, y, width, height],
// class: 'person',
// score: 0.95
// }
});
// Dibujar bounding boxes
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
predictions.forEach(pred => {
ctx.strokeStyle = '#00FF00';
ctx.lineWidth = 2;
ctx.strokeRect(...pred.bbox);
ctx.fillStyle = '#00FF00';
ctx.fillText(
`${pred.class} (${Math.round(pred.score * 100)}%)`,
pred.bbox[0],
pred.bbox[1] - 5
);
});
}
Detección de Pose Con PoseNet/MoveNet
Uno de los casos de uso más populares es la detección de pose corporal.
Implementación Básica
import * as poseDetection from '@tensorflow-models/pose-detection';
async function detectPose() {
// Configura el detector
const detectorConfig = {
modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING
};
const detector = await poseDetection.createDetector(
poseDetection.SupportedModels.MoveNet,
detectorConfig
);
// Usa video de la webcam
const video = document.getElementById('webcam');
async function detect() {
const poses = await detector.estimatePoses(video);
if (poses.length > 0) {
const pose = poses[0];
// Keypoints del cuerpo
pose.keypoints.forEach(keypoint => {
if (keypoint.score > 0.5) {
console.log(`${keypoint.name}: (${keypoint.x}, ${keypoint.y})`);
// nose: (320, 180)
// left_eye: (310, 170)
// right_shoulder: (280, 250)
// etc.
}
});
}
requestAnimationFrame(detect);
}
detect();
}Aplicación Práctica: Contador de Ejercicios
class ExerciseCounter {
constructor() {
this.squatCount = 0;
this.isDown = false;
}
detectSquat(pose) {
const leftHip = pose.keypoints.find(k => k.name === 'left_hip');
const leftKnee = pose.keypoints.find(k => k.name === 'left_knee');
const leftAnkle = pose.keypoints.find(k => k.name === 'left_ankle');
if (!leftHip || !leftKnee || !leftAnkle) return;
// Calcula ángulo de la rodilla
const angle = this.calculateAngle(
leftHip,
leftKnee,
leftAnkle
);
// Lógica de conteo
if (angle < 90 && !this.isDown) {
this.isDown = true;
} else if (angle > 160 && this.isDown) {
this.isDown = false;
this.squatCount++;
this.onSquatComplete(this.squatCount);
}
}
calculateAngle(a, b, c) {
const radians = Math.atan2(c.y - b.y, c.x - b.x) -
Math.atan2(a.y - b.y, a.x - b.x);
let angle = Math.abs(radians * 180 / Math.PI);
if (angle > 180) angle = 360 - angle;
return angle;
}
onSquatComplete(count) {
console.log(`¡Sentadilla #${count} completa!`);
}
}
Reconocimiento de Gestos de Mano
Otro caso de uso poderoso es detectar gestos de las manos.
import * as handPoseDetection from '@tensorflow-models/hand-pose-detection';
async function detectHands() {
const model = handPoseDetection.SupportedModels.MediaPipeHands;
const detectorConfig = {
runtime: 'tfjs',
maxHands: 2
};
const detector = await handPoseDetection.createDetector(
model,
detectorConfig
);
const video = document.getElementById('webcam');
async function detect() {
const hands = await detector.estimateHands(video);
hands.forEach(hand => {
// 21 keypoints por mano
const landmarks = hand.keypoints;
// Detectar gesto de "pulgar arriba"
if (isThumbsUp(landmarks)) {
console.log('👍 ¡Pulgar arriba detectado!');
}
// Detectar gesto de "paz"
if (isPeaceSign(landmarks)) {
console.log('✌️ ¡Paz detectado!');
}
});
requestAnimationFrame(detect);
}
detect();
}
function isThumbsUp(landmarks) {
const thumbTip = landmarks[4];
const thumbBase = landmarks[2];
const indexTip = landmarks[8];
// Pulgar para arriba y otros dedos cerrados
const thumbUp = thumbTip.y < thumbBase.y;
const indexDown = indexTip.y > landmarks[5].y;
return thumbUp && indexDown;
}
function isPeaceSign(landmarks) {
const indexTip = landmarks[8];
const middleTip = landmarks[12];
const ringTip = landmarks[16];
// Índice y medio estirados, otros cerrados
const indexUp = indexTip.y < landmarks[5].y;
const middleUp = middleTip.y < landmarks[9].y;
const ringDown = ringTip.y > landmarks[13].y;
return indexUp && middleUp && ringDown;
}
Clasificación de Texto Con Universal Sentence Encoder
TensorFlow.js no es solo para imágenes - funciona óptimo con texto.
import * as use from '@tensorflow-models/universal-sentence-encoder';
async function analyzeText() {
const model = await use.load();
// Embeddings de texto
const sentences = [
'¡Adoré este producto, muy bueno!',
'Pésimo, no recomiendo.',
'El servicio fue ok, nada excepcional.'
];
const embeddings = await model.embed(sentences);
console.log(embeddings.shape); // [3, 512]
// Clasificación de sentimiento simple
const positiveRef = await model.embed(['Excelente, maravilloso, perfecto']);
const negativeRef = await model.embed(['Terrible, horrible, pésimo']);
sentences.forEach(async (sentence, i) => {
const sentenceEmbed = embeddings.slice([i, 0], [1, 512]);
const positiveSim = cosineSimilarity(sentenceEmbed, positiveRef);
const negativeSim = cosineSimilarity(sentenceEmbed, negativeRef);
const sentiment = positiveSim > negativeSim ? 'Positivo' : 'Negativo';
console.log(`"${sentence}" -> ${sentiment}`);
});
}
function cosineSimilarity(a, b) {
const dotProduct = tf.sum(tf.mul(a, b));
const normA = tf.norm(a);
const normB = tf.norm(b);
return dotProduct.div(normA.mul(normB)).dataSync()[0];
}
Entrenando Tu Propio Modelo
Además de usar modelos pre-entrenados, puedes crear los tuyos.
Modelo Simple de Clasificación
import * as tf from '@tensorflow/tfjs';
async function trainModel() {
// Datos de ejemplo: XOR problem
const xs = tf.tensor2d([[0, 0], [0, 1], [1, 0], [1, 1]]);
const ys = tf.tensor2d([[0], [1], [1], [0]]);
// Arquitectura del modelo
const model = tf.sequential({
layers: [
tf.layers.dense({
inputShape: [2],
units: 8,
activation: 'relu'
}),
tf.layers.dense({
units: 4,
activation: 'relu'
}),
tf.layers.dense({
units: 1,
activation: 'sigmoid'
})
]
});
// Compilar
model.compile({
optimizer: tf.train.adam(0.1),
loss: 'binaryCrossentropy',
metrics: ['accuracy']
});
// Entrenar
await model.fit(xs, ys, {
epochs: 100,
callbacks: {
onEpochEnd: (epoch, logs) => {
console.log(`Epoch ${epoch}: loss = ${logs.loss.toFixed(4)}`);
}
}
});
// Testar
const prediction = model.predict(tf.tensor2d([[0, 1]]));
prediction.print(); // ~0.95 (próximo a 1)
// Guardar modelo
await model.save('localstorage://mi-modelo');
}
// Cargar modelo guardado
async function loadModel() {
const model = await tf.loadLayersModel('localstorage://mi-modelo');
return model;
}Transfer Learning Con MobileNet
async function customImageClassifier() {
// Carga MobileNet sin la capa final
const mobilenet = await tf.loadLayersModel(
'https://tfhub.dev/google/tfjs-model/mobilenet_v2_100_224/feature_vector/3/default/1',
{ fromTFHub: true }
);
// Congela pesos de MobileNet
mobilenet.trainable = false;
// Agrega capas customizadas
const model = tf.sequential();
model.add(mobilenet);
model.add(tf.layers.dense({
units: 128,
activation: 'relu'
}));
model.add(tf.layers.dropout({ rate: 0.5 }));
model.add(tf.layers.dense({
units: 3, // Número de clases
activation: 'softmax'
}));
model.compile({
optimizer: tf.train.adam(0.0001),
loss: 'categoricalCrossentropy',
metrics: ['accuracy']
});
return model;
}
Optimización de Performance
Para ML en el browser funcionar bien, optimización es crucial.
Backend WebGL vs CPU
// Verificar backend actual
console.log(tf.getBackend()); // 'webgl' o 'cpu'
// Forzar WebGL (más rápido)
await tf.setBackend('webgl');
// O usar WebGPU (más rápido aún, si disponible)
if (navigator.gpu) {
await tf.setBackend('webgpu');
}Cuantización de Modelos
// Al convertir modelo Python para TensorFlow.js
// Usa cuantización para reducir tamaño
// tensorflowjs_converter \
// --input_format=tf_saved_model \
// --output_format=tfjs_graph_model \
// --quantize_float16=* \
// ./saved_model \
// ./tfjs_model
// Resultado: modelo 50% menor, performance similarWarm-up del Modelo
async function warmupModel(model) {
// Primera inferencia es más lenta
// Haz "warm-up" antes de uso real
const dummyInput = tf.zeros([1, 224, 224, 3]);
const warmupResult = model.predict(dummyInput);
warmupResult.dispose();
dummyInput.dispose();
console.log('¡Modelo calentado y listo!');
}
Aplicación Completa: Clasificador en Tiempo Real
Vamos a juntar todo en una aplicación funcional.
import * as tf from '@tensorflow/tfjs';
import * as mobilenet from '@tensorflow-models/mobilenet';
class RealTimeClassifier {
constructor(videoElement, resultsElement) {
this.video = videoElement;
this.results = resultsElement;
this.model = null;
this.isRunning = false;
}
async init() {
// Configura WebGL
await tf.setBackend('webgl');
// Carga modelo
console.log('Cargando modelo...');
this.model = await mobilenet.load({
version: 2,
alpha: 1.0
});
// Calienta modelo
await this.warmup();
// Inicia webcam
await this.setupCamera();
console.log('¡Listo!');
}
async setupCamera() {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' }
});
this.video.srcObject = stream;
await new Promise(resolve => {
this.video.onloadedmetadata = resolve;
});
this.video.play();
}
async warmup() {
const dummyInput = tf.zeros([1, 224, 224, 3]);
const result = this.model.infer(dummyInput, 'conv_preds');
result.dispose();
dummyInput.dispose();
}
start() {
this.isRunning = true;
this.detect();
}
stop() {
this.isRunning = false;
}
async detect() {
if (!this.isRunning) return;
const predictions = await this.model.classify(this.video, 3);
this.displayResults(predictions);
requestAnimationFrame(() => this.detect());
}
displayResults(predictions) {
this.results.innerHTML = predictions
.map(p => `
<div class="prediction">
<span class="label">${p.className}</span>
<span class="score">${(p.probability * 100).toFixed(1)}%</span>
<div class="bar" style="width: ${p.probability * 100}%"></div>
</div>
`)
.join('');
}
}
// Uso
const classifier = new RealTimeClassifier(
document.getElementById('video'),
document.getElementById('results')
);
document.getElementById('startBtn').onclick = async () => {
await classifier.init();
classifier.start();
};Conclusión
TensorFlow.js democratiza el acceso a machine learning, permitiendo que cualquier desarrollador JavaScript cree aplicaciones inteligentes sin precisar infraestructura de servidor o conocimiento profundo de Python.
En 2025, con GPUs más poderosas en los dispositivos y modelos cada vez más optimizados, el browser se torna una plataforma legítima para ML - ofreciendo privacidad, performance y escala sin costo de infraestructura.
Si quieres explorar otras formas de mejorar performance en el browser, te recomiendo echar un vistazo al artículo WebAssembly y JavaScript: Performance Extrema donde exploramos cómo combinar estas tecnologías.

