TensorFlow Lite Models on Flutter Web: What Actually Works
tflite_flutter uses FFI and stops at the browser edge. The two real web paths for a .tflite model: convert it to TensorFlow.js, or run it in WASM via @tensorflow/tfjs-tflite.
Published on • November 11, 2026
AI Assistant

TensorFlow Lite runs beautifully on phones and desktops, and it does not run at all in a browser — which surprises anyone who shipped a tflite_flutter feature to mobile first. The Flutter plugin binds the TFLite C API over FFI, browsers have no FFI, and there is no web target in its platform table. This post covers what actually works when a Flutter web app has to load that .tflite file, and the two routes to get there.
Why tflite_flutter stops at the browser edge
tflite_flutter (0.12.1, published by the TensorFlow team) supports Android, iOS, Linux, macOS, and Windows. Its Interpreter is generated FFI glue over libtensorflowlite_c, with delegates like XNNPACK and GPU loaded as native libraries. A browser cannot load those .so/.dll files, so importing the package in a web build gets you a runtime failure, not a fallback.
The web story therefore runs on TensorFlow.js, which has its own way to execute TFLite models. Everything below keeps your Dart code compiled for web unchanged — only the model loading layer moves.
Route 1: convert the model to TensorFlow.js
tensorflowjs_converter converts TensorFlow formats into the graph model JSON plus weight shards that @tensorflow/tfjs loads:
pip install tensorflowjs
tensorflowjs_converter \
--input_format=tf_saved_model \
--output_format=tfjs_graph_model \
saved_model_dir/ assets/models/model_web/
The converter eats SavedModel and Keras formats, not the .tflite FlatBuffer itself — so run it from your original Python model before you export for mobile. It emits model.json and one or more .bin shards; declare both as Flutter assets and let the browser fetch them relative to the JSON.
// pubspec.yaml
flutter:
assets:
- assets/models/model_web/model.json
- assets/models/model_web/group1-shard1of1.bin
Once loaded, pick the backend explicitly: WebGL is the safe default, and WebGPU is worth trying where the browser exposes it (see the WebGPU post for detection).
if (!(await tf.setBackend('webgpu'))) await tf.setBackend('webgl');
await tf.ready();
const model = await tf.loadGraphModel('/assets/models/model_web/model.json');
Route 2: keep the .tflite and run it in WASM
If you only have the .tflite file, @tensorflow/tfjs-tflite packages the TFLite C++ runtime as WASM modules and runs the model through a TFJS-compatible API:
import '@tensorflow/tfjs-backend-cpu';
import * as tf from '@tensorflow/tfjs-core';
import * as tflite from '@tensorflow/tfjs-tflite';
const model = await tflite.loadTFLiteModel('/assets/models/mobilenet.tflite', {
numThreads: navigator.hardwareConcurrency / 2,
});
const output = model.predict(inputTensor) as tf.Tensor;
Under the hood it picks the best WASM binary for the environment (SIMD, multi-threading) and accelerates with XNNPACK. Multi-threaded WASM only kicks in under cross-origin isolation, so without COOP/COEP headers you silently get the single-threaded build. Be aware the package is explicit about its status: version 0.0.1-alpha.10 is marked WORK IN PROGRESS — fine for a feature you control, a poor foundation for a product bet.
Call it from Dart through one bridge
Whichever route you pick, keep a single global in web/ and reach it with dart:js_interop. Flutter web serves declared assets at their path, so the model URL is just a string:
import 'dart:js_interop';
import 'dart:js_interop_unsafe';
import 'package:web/web.dart' as web;
Future<Float32List> classify(web.HTMLImageElement image) async {
final bridge = web.window.getProperty('tfliteBridge'.toJS)! as JSObject;
final pending = bridge.callMethod('run'.toJS, image.toJS)! as JSPromise;
final output = await pending.toDart as JSFloat32Array;
return output.toDart;
}
Preprocessing belongs on the JS side: draw and resize the image into a canvas there, then read pixels with tf.browser.fromPixels — that keeps pixel work out of Dart and matches what the model expects.
Shapes, memory, and performance
Get the input contract exactly right. A classifier expecting [1, 224, 224, 3] float32 normalized to [-1, 1] will not tolerate [1, 224, 224, 3] float32 in [0, 1] — it will run happily and return garbage. Read the shape off the model instead of hard-coding it:
const input = model.inputs[0];
console.log(input.shape, input.dtype); // e.g. [1,224,224,3] float32
Dispose every intermediate tensor; in a loop that is the difference between a stable tab and one that climbs until the browser kills it. For throughput, batch where the model allows it, prefer quantized models (uint8 weights shrink the download 4x), and push repeated inference into a Web Worker — the pattern from the ML inference post applies unchanged here.
Takeaway: tflite_flutter cannot cross into the browser — convert your SavedModel to a TFJS graph model, or run the .tflite itself through the alpha tfjs-tflite WASM runtime, and keep the Dart side behind a thin JS bridge with exact tensor shapes.