Deploy Your First TensorFlow.js Model on a Static Site – A Complete Tutorial
Read this article in clean Markdown format for LLMs and AI context.Ever wondered why you can’t just drop a machine‑learning model onto a plain HTML page and watch it work? The answer is simple: you need a few extra steps to make the browser happy. In today’s fast‑moving web world, being able to run AI right in the client’s browser opens doors to privacy‑first apps, instant feedback, and lower server costs. If you’re curious about other client‑side AI options, check out the guide on adding a custom ChatGPT widget to a React site.
Why TensorFlow.js on a Static Site?
TensorFlow.js lets you run pre‑trained models directly in the browser using JavaScript. No Node server, no Python runtime, just HTML, CSS, and a bit of JS. This is perfect for:
- Privacy – data never leaves the user’s device.
- Speed – no round‑trip to a remote API.
- Cost – you can host the whole thing on a free static‑site provider.
When deciding which library to use, see our Choosing the Right AI Toolkit for Web Developers: A Practical Checklist for a quick comparison.
I first tried this on a coffee‑break project that guessed the sentiment of a tweet. The whole thing ran on a single HTML file, and my friends were amazed that “the web page itself” was doing the heavy lifting.
Overview of the Steps
- Train a tiny model (or grab a pre‑trained one).
- Convert it to TensorFlow.js format.
- Add the model files to your static site folder.
- Write JavaScript that loads and uses the model.
- Deploy the site.
We’ll use a simple image‑classification model that distinguishes a cat from a dog. The model will be tiny enough to load in a few hundred kilobytes, keeping the page snappy.
Step 1 – Train a Tiny Model
If you already have a model, skip ahead. Otherwise, fire up a Python notebook and run the following (you can also do this locally).
import tensorflow as tf
from tensorflow.keras import layers, models
import numpy as np
# Create a very small CNN
def create_model():
model = models.Sequential([
layers.Conv2D(8, (3,3), activation='relu', input_shape=(64,64,3)),
layers.MaxPooling2D(),
layers.Conv2D(16, (3,3), activation='relu'),
layers.MaxPooling2D(),
layers.Flatten(),
layers.Dense(2, activation='softmax')
])
model.compile(optimizer='adam',
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
return model
model = create_model()
# Assume X_train, y_train are prepared 64x64 RGB images
# model.fit(X_train, y_train, epochs=5, validation_split=0.2)
Keep the model shallow; the goal is to demonstrate deployment, not to win Kaggle. After training, save it:
model.save('cat_dog_model')
Step 2 – Convert to TensorFlow.js Format
Install the TensorFlow.js converter (you only need this once).
npm install -g @tensorflow/tfjs-converter
Then run:
tensorflowjs_converter --input_format=tf_saved_model \
./cat_dog_model ./web_model
The command creates a folder web_model containing model.json and a few binary weight files (group1-shard...). These are what the browser will load.
Step 3 – Set Up the Static Site
Create a folder called site. Inside, add three files:
index.html– the page UI.script.js– code that loads the model and runs inference.style.css– optional styling.
Copy the entire web_model folder into site. Your structure should look like:
site/
│─ index.html
│─ script.js
│─ style.css
│─ web_model/
│─ model.json
│─ group1-shard1of1.bin
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Cat vs Dog – TensorFlow.js Demo</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Cat or Dog?</h1>
<input type="file" id="imgInput" accept="image/*">
<canvas id="preview" width="64" height="64"></canvas>
<p id="result">Pick an image to see the prediction.</p>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/[email protected]"></script>
<script src="script.js"></script>
</body>
</html>
style.css (optional)
body { font-family: Arial, sans-serif; text-align: center; margin-top: 40px; }
canvas { border: 1px solid #ccc; margin-top: 10px; }
Step 4 – Write the JavaScript Loader
Open script.js and add the following. I kept the code deliberately simple so you can follow each line.
let model;
// Load the model as soon as the page starts
(async function() {
console.log('Loading model...');
model = await tf.loadLayersModel('web_model/model.json');
console.log('Model loaded.');
})();
// Helper to resize the image to 64x64 and get a tensor
function preprocessImage(img) {
const tensor = tf.browser.fromPixels(img)
.resizeNearestNeighbor([64, 64]) // match training size
.toFloat()
.div(tf.scalar(255.0)) // normalize to [0,1]
.expandDims(); // add batch dimension
return tensor;
}
// When user selects a file
document.getElementById('imgInput').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
const img = new Image();
img.onload = async () => {
// Draw to canvas for preview
const canvas = document.getElementById('preview');
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, 64, 64);
// Run inference
const input = preprocessImage(canvas);
const prediction = await model.predict(input).data();
const classes = ['Cat', 'Dog'];
const best = prediction[0] > prediction[1] ? 0 : 1;
document.getElementById('result').innerText =
`I think it's a ${classes[best]} (${(prediction[best]*100).toFixed(1)}% sure)`;
};
img.src = URL.createObjectURL(file);
});
A quick note on the code:
tf.loadLayersModelfetchesmodel.jsonand the binary weight files. Because we’re on a static site, the browser treats them like any other asset.preprocessImagemirrors the preprocessing we did during training: resize, convert to float, normalize, and add a batch dimension.- The prediction result is a simple array of two probabilities. We pick the larger one and display it.
Step 5 – Deploy the Site
Because everything is static, you can push the site folder to any host that serves plain files. Here’s a quick Netlify example:
- Create a new site from Git.
- Point Netlify to the
sitefolder as the publish directory. - Hit “Deploy”.
Or, if you prefer GitHub Pages:
git init
git add .
git commit -m "first deploy"
git branch -M main
git remote add origin https://github.com/yourname/cat-dog-demo.git
git push -u origin main
Then enable GitHub Pages in the repo settings, selecting the main branch root. Within a minute, your model will be live at https://yourname.github.io/cat-dog-demo/.
If you’d rather run the model from a serverless endpoint instead of a static page, see the walkthrough on how to Deploy a TensorFlow model to a serverless function in 10 minutes.
Testing It Out
Open the page, click “Choose File”, and select a picture of a cat or a dog. The canvas will shrink the image to 64×64, the model will run in the browser, and you’ll see a confidence score. No server logs, no latency beyond the tiny model load. If the page feels sluggish, double‑check that the model size is small; you can always prune layers or use quantization during conversion (--quantization_bytes=1).
Common Pitfalls and How to Fix Them
| Problem | Likely Cause | Fix |
|---|---|---|
| Model fails to load (404) | Wrong path to model.json |
Ensure the path in loadLayersModel matches the folder structure. |
| Prediction always returns the same class | Input not normalized | Verify the div(tf.scalar(255.0)) step is present. |
| Browser shows “Cross‑origin request blocked” | Hosting on a domain without proper CORS headers | Most static hosts set CORS correctly; if you use a custom server, add Access-Control-Allow-Origin: *. |
A Little Personal Reflection
When I first tried to embed a TensorFlow model in a static page, I spent an hour chasing a mysterious “undefined is not a function” error. Turns out I had forgotten to include the TensorFlow.js script tag before my own script. A simple ordering mistake, but it reminded me that even in a world of sophisticated AI, the basics—like script order—still matter. Keep your HTML tidy, and the browser will thank you.
That’s it! You now have a fully functional AI demo that lives on a static site. Feel free to swap the cat‑dog model for anything else—handwritten digit recognizers, sentiment classifiers, or even a tiny voice‑command detector. The pipeline stays the same, and the possibilities are as wide as your imagination.
- →
- →
- →
- →
- →