
How Small Can a Browser-Based Model Really Get?
A few days ago I came across Google’s launch post for LiteRT.js, their new JavaScript runtime for running .tflite models directly in the browser. The numbers are hard to skim past: up to 3x faster than other web inference runtimes, and 5-60x on GPU/NPU versus plain CPU, with WebGPU, WASM/XNNPACK, and an experimental WebNN backend all supported out of the box.
Benchmark numbers are one thing. I wanted to know if this held up on something real: a full pipeline with multiple models, real accuracy constraints, and a genuinely offline requirement, not a synthetic detection demo. So over a few evenings I built a small proof of concept: a face check-in flow that runs entirely in the browser, works with the network switched off, and downloads in a footprint small enough that “download and go” is a legitimate description rather than marketing copy.
This post walks through that PoC: the pipeline shape, the model I picked (and the one I almost picked instead), a liveness bug that would have made the whole thing unusable, and where I’d take it next.
Why “detect, align, embed” and not one model
The instinctive shortcut is a single model that takes a raw webcam frame and spits out “this is Alice.” Doesn’t work well. Face embedding models expect a tightly cropped, aligned input, typically something like 112×112 pixels, face centered, roughly frontal. Feed one a raw frame straight off a webcam and accuracy falls off a cliff.
So the pipeline is really three stages:
- Detection + landmarks. A small model finds the face and locates key points (eyes, nose, mouth corners). I used a MediaPipe-class face landmarker here, a few hundred KB to a couple of MB as
.tflite, fast enough to run every frame on CPU alone. - Alignment. Pure geometry, no model involved. The landmarks tell you how to crop and rotate the face into the pose the embedder expects.
- Embedding extraction. The actual “who is this” model, and the one worth spending time on.
Nice side effect of this shape: the same landmarks used for alignment also drive the liveness signal later. One model, two jobs.
Picking the embedding model, and a license detour worth mentioning
For the embedding step I wanted something in the MobileFaceNet class: small, ArcFace-trained, built for edge deployment rather than server-side face search at web scale.
My first instinct was one of the InsightFace pretrained checkpoints, the most commonly cited starting point in face-embedding writeups. Worth checking the license before you get attached to a checkpoint, though. A good chunk of InsightFace’s pretrained weights, including the smaller MobileFaceNet-class ones, ship under a non-commercial research license. Fine for a paper. Not something I wanted to build on, even for a proof of concept.
I ended up on SFace, an Apache-2.0-licensed MobileFaceNet-class model out of the OpenCV Zoo, built for exactly this size and licensing niche. It ships as ONNX, which meant a short conversion path: ONNX to TFLite, then quantized to int8.
That’s where the size story gets interesting. The fp32 version of a model like this runs 4–5 MB. Quantized to int8, it drops to roughly 1.2–1.5 MB. A full ResNet50-ArcFace model, the kind you’d actually want for open-set face search at real scale, comes in around 160 MB. Two orders of magnitude apart, for a problem where a PoC-sized gallery of a few hundred to a couple thousand identities doesn’t need anywhere near that much model.

One flag for anyone doing this themselves: quantization can silently wreck an embedding model’s geometry even when the model still “looks” like it works. The fix is boring but necessary. Run the same fixed set of images through both the original and the quantized model, and check same-image cosine similarity stays above roughly 0.99 before trusting the smaller file.
Getting the whole thing to run with the network off
This was the part I was most curious about after reading Google’s post, since “zero server costs” and “entirely locally” are the headline claims.
Getting there is mostly a matter of being deliberate about where every byte comes from:
- The
.tflitemodels and the LiteRT.js WASM runtime assets are served same-origin, not pulled from a CDN. Once the page has loaded once, nothing depends on the outside world. - Threading is opt-in (
loadLiteRt(path, { threads: true })), and I found it made no measurable difference for a model this small. Worth checking for your own model size before assuming you need it. - Backend selection is WebGPU when available, falling back to WASM/XNNPACK when it isn’t. No rigorous benchmark on my end, but the WebGPU path was noticeably snappier on the machine I tested on, which lines up directionally with Google’s claimed GPU speedup even without a number attached.
The actual proof isn’t a claim in a blog post, though. It’s a face getting detected and matched with the machine’s Wi-Fi visibly switched off. That’s the short video attached here: detection, alignment, matching, and a liveness pass, no network connection in sight.

Liveness: not optional once the flow is automated
If a human reviews every match before anything happens, a weak liveness check is a tolerable gap. If the flow is fully automated, camera sees a face, matches it, acts, then a liveness check that a printed photo or a phone screen can fool isn’t a corner case. It’s the whole security model.
I went with a layered approach: multi-frame consistency (one lucky frame isn’t enough to count as a match) plus an active challenge: blink detection and a head-turn prompt, reusing the landmarks from the detection stage.
Worth admitting the bug I shipped in my first pass. The “turn left” and “turn right” prompts were backwards. The camera view is mirrored, like any selfie-style camera, but the yaw calculation was done in image space rather than the subject’s own frame of reference, so “turn left” was only satisfied by physically turning right. It worked fine in my own testing, because I’d unconsciously learned to compensate for it, which is exactly the kind of bug that survives testing by the person who wrote the code and falls apart the moment someone else tries it. Flipping the comparison fixed it. Good reminder to test liveness prompts on a person who didn’t write the liveness code.
Matching stays local too
Last piece: matching the live embedding against a gallery of known embeddings. At a gallery size in the hundreds to low thousands, cosine similarity search against every stored embedding is sub-millisecond in plain JavaScript. No approximate nearest-neighbor index needed at this scale, and no round trip to a server on a path where latency actually matters to the person standing in front of the camera.

What this actually gets you
- A total model download in the low single-digit megabytes. Detector plus embedder together, comfortably under 5 MB.
- Fully offline operation after the first load, no server-side inference at all.
- Camera frames that never leave the browser. Only embeddings would ever need to cross a network boundary, and in this PoC nothing did.
- Real-time performance on ordinary consumer hardware, no dedicated GPU required.
None of that is a novel claim by itself. What was genuinely interesting was how little of it required fighting the tooling. LiteRT.js’s job is getting a .tflite model running in a browser tab with sensible backend selection, and it does that part well. Most of the actual effort went into what would be true regardless of runtime: picking a model with the right size/accuracy/license trade-off, getting alignment right, and making liveness work for someone other than me.
Where I’d take this next
- A passive anti-spoof model. A small texture/frequency-based liveness model (~2 MB, MiniFASNet-class) layered on top of the active challenge, catching screen-replay attacks with no extra step for the user.
- A larger embedding variant as a fallback, if a bigger gallery or harsher lighting ever pushed accuracy below what’s acceptable. There’s a straightforward path to a 512-d model at 2–4 MB quantized, still small by any reasonable standard.
- Leaning further into WebGPU as browser support widens, especially for the detector, since it’s the model running on every frame.
- The same pipeline shape for other kiosk-style, walk-up authentication flows. “Detect, align, embed, match, verify liveness” isn’t specific to faces conceptually, and Google’s own use-case list for LiteRT.js (object detection, depth estimation, image upscaling) is a reminder that this runtime is built for more than one kind of model.
Google’s numbers were the trigger for building this. The actual takeaway turned out smaller and more useful: a model doesn’t need to be big to be good enough, and running it entirely in the browser isn’t the compromise it used to be.
This story was first published here on medium.



