Back to blog

TensorFlow.js: Machine Learning Directly in the Browser With JavaScript

Hello HaWkers, what if you could run machine learning models directly in the user's browser, without needing a server? In 2025, this is no longer the future - it's reality, and TensorFlow.js is the tool that makes it possible.

In this guide, we'll explore how to use TensorFlow.js to create intelligent applications that run 100% client-side.

Why Machine Learning in the Browser

Running ML in the browser offers unique advantages that many developers still don't explore.

Benefits

Privacy:

  • Data never leaves the user's device
  • No LGPD/GDPR concerns for processing
  • Ideal for sensitive applications

Performance:

  • No network latency for inference
  • Uses device GPU via WebGL
  • Real-time responses

Cost:

  • Zero server cost for inference
  • Unlimited scale without increasing infrastructure
  • Democratizes ML access

Experience:

  • Works offline
  • Real-time interactivity
  • Doesn't depend on connection

🧠 Context: TensorFlow.js is used by millions of developers and companies like Google, Spotify, and Airbnb for browser ML.

Getting Started With TensorFlow.js

Let's set up a project and understand the basic concepts.

Installation

# Via NPM
npm install @tensorflow/tfjs

# Or via CDN in HTML
# <script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>

Fundamental Concepts

import * as tf from '@tensorflow/tfjs';

// Tensors: Multidimensional arrays
const tensor1D = tf.tensor([1, 2, 3, 4]);
const tensor2D = tf.tensor([[1, 2], [3, 4]]);

// Shapes: Tensor dimensions
console.log(tensor2D.shape); // [2, 2]

// Basic operations
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

// IMPORTANT: Clean up memory
tensor1D.dispose();
tensor2D.dispose();
// Or use tf.tidy() for automatic cleanup

Memory Management

// tf.tidy() automatically cleans intermediate tensors
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; // Only d is kept
});

// Check memory usage
console.log(tf.memory());
// { numTensors: X, numDataBuffers: Y, numBytes: Z }

Using Pre-Trained Models

The fastest way to get started is using already trained models.

Image Classification With MobileNet

import * as tf from '@tensorflow/tfjs';
import * as mobilenet from '@tensorflow-models/mobilenet';

async function classifyImage() {
  // Load the model (first time might take a while)
  const model = await mobilenet.load();

  // Image element from DOM
  const img = document.getElementById('myImage');

  // Classify the image
  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 }
  // ]
}

Object Detection With 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
    // }
  });

  // Draw 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
    );
  });
}

Pose Detection With PoseNet/MoveNet

One of the most popular use cases is body pose detection.

Basic Implementation

import * as poseDetection from '@tensorflow-models/pose-detection';

async function detectPose() {
  // Configure detector
  const detectorConfig = {
    modelType: poseDetection.movenet.modelType.SINGLEPOSE_LIGHTNING
  };

  const detector = await poseDetection.createDetector(
    poseDetection.SupportedModels.MoveNet,
    detectorConfig
  );

  // Use webcam video
  const video = document.getElementById('webcam');

  async function detect() {
    const poses = await detector.estimatePoses(video);

    if (poses.length > 0) {
      const pose = poses[0];

      // Body keypoints
      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();
}

Practical Application: Exercise Counter

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;

    // Calculate knee angle
    const angle = this.calculateAngle(
      leftHip,
      leftKnee,
      leftAnkle
    );

    // Counting logic
    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(`Squat #${count} complete!`);
  }
}

Hand Gesture Recognition

Another powerful use case is detecting hand gestures.

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 per hand
      const landmarks = hand.keypoints;

      // Detect "thumbs up" gesture
      if (isThumbsUp(landmarks)) {
        console.log('👍 Thumbs up detected!');
      }

      // Detect "peace" gesture
      if (isPeaceSign(landmarks)) {
        console.log('✌️ Peace sign detected!');
      }
    });

    requestAnimationFrame(detect);
  }

  detect();
}

function isThumbsUp(landmarks) {
  const thumbTip = landmarks[4];
  const thumbBase = landmarks[2];
  const indexTip = landmarks[8];

  // Thumb up and other fingers closed
  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];

  // Index and middle extended, others closed
  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;
}

Text Classification With Universal Sentence Encoder

TensorFlow.js isn't just for images - it works great with text.

import * as use from '@tensorflow-models/universal-sentence-encoder';

async function analyzeText() {
  const model = await use.load();

  // Text embeddings
  const sentences = [
    'I loved this product, very good!',
    'Terrible, do not recommend.',
    'The service was ok, nothing exceptional.'
  ];

  const embeddings = await model.embed(sentences);
  console.log(embeddings.shape); // [3, 512]

  // Simple sentiment classification
  const positiveRef = await model.embed(['Excellent, wonderful, perfect']);
  const negativeRef = await model.embed(['Terrible, horrible, awful']);

  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 ? 'Positive' : 'Negative';
    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];
}

Training Your Own Model

Besides using pre-trained models, you can create your own.

Simple Classification Model

import * as tf from '@tensorflow/tfjs';

async function trainModel() {
  // Example data: XOR problem
  const xs = tf.tensor2d([[0, 0], [0, 1], [1, 0], [1, 1]]);
  const ys = tf.tensor2d([[0], [1], [1], [0]]);

  // Model architecture
  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'
      })
    ]
  });

  // Compile
  model.compile({
    optimizer: tf.train.adam(0.1),
    loss: 'binaryCrossentropy',
    metrics: ['accuracy']
  });

  // Train
  await model.fit(xs, ys, {
    epochs: 100,
    callbacks: {
      onEpochEnd: (epoch, logs) => {
        console.log(`Epoch ${epoch}: loss = ${logs.loss.toFixed(4)}`);
      }
    }
  });

  // Test
  const prediction = model.predict(tf.tensor2d([[0, 1]]));
  prediction.print(); // ~0.95 (close to 1)

  // Save model
  await model.save('localstorage://my-model');
}

// Load saved model
async function loadModel() {
  const model = await tf.loadLayersModel('localstorage://my-model');
  return model;
}

Transfer Learning With MobileNet

async function customImageClassifier() {
  // Load MobileNet without final layer
  const mobilenet = await tf.loadLayersModel(
    'https://tfhub.dev/google/tfjs-model/mobilenet_v2_100_224/feature_vector/3/default/1',
    { fromTFHub: true }
  );

  // Freeze MobileNet weights
  mobilenet.trainable = false;

  // Add custom layers
  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, // Number of classes
    activation: 'softmax'
  }));

  model.compile({
    optimizer: tf.train.adam(0.0001),
    loss: 'categoricalCrossentropy',
    metrics: ['accuracy']
  });

  return model;
}

Performance Optimization

For ML in the browser to work well, optimization is crucial.

WebGL vs CPU Backend

// Check current backend
console.log(tf.getBackend()); // 'webgl' or 'cpu'

// Force WebGL (faster)
await tf.setBackend('webgl');

// Or use WebGPU (even faster, if available)
if (navigator.gpu) {
  await tf.setBackend('webgpu');
}

Model Quantization

// When converting Python model to TensorFlow.js
// Use quantization to reduce size

// tensorflowjs_converter \
//   --input_format=tf_saved_model \
//   --output_format=tfjs_graph_model \
//   --quantize_float16=* \
//   ./saved_model \
//   ./tfjs_model

// Result: 50% smaller model, similar performance

Model Warm-up

async function warmupModel(model) {
  // First inference is slower
  // Do "warm-up" before real use

  const dummyInput = tf.zeros([1, 224, 224, 3]);
  const warmupResult = model.predict(dummyInput);
  warmupResult.dispose();
  dummyInput.dispose();

  console.log('Model warmed up and ready!');
}

Complete Application: Real-Time Classifier

Let's put everything together in a functional application.

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() {
    // Configure WebGL
    await tf.setBackend('webgl');

    // Load model
    console.log('Loading model...');
    this.model = await mobilenet.load({
      version: 2,
      alpha: 1.0
    });

    // Warm up model
    await this.warmup();

    // Start webcam
    await this.setupCamera();

    console.log('Ready!');
  }

  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('');
  }
}

// Usage
const classifier = new RealTimeClassifier(
  document.getElementById('video'),
  document.getElementById('results')
);

document.getElementById('startBtn').onclick = async () => {
  await classifier.init();
  classifier.start();
};

Conclusion

TensorFlow.js democratizes access to machine learning, allowing any JavaScript developer to create intelligent applications without needing server infrastructure or deep Python knowledge.

In 2025, with more powerful GPUs in devices and increasingly optimized models, the browser becomes a legitimate platform for ML - offering privacy, performance, and scale without infrastructure cost.

If you want to explore other ways to improve browser performance, I recommend checking out the article WebAssembly and JavaScript: Extreme Performance where we explore how to combine these technologies.

Let's go! 🦅

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments