Lottie Animations on Flutter Web: Loading, Rendering, and Performance
Lottie on Flutter web: how the pure-Dart runtime draws through the canvas path, loading options, bundle size, RepaintBoundary, frame budget, reduced motion.
Published on • November 12, 2026
AI Assistant

Lottie is the easy animation bet on Flutter web, and the reason is that lottie 3.8.2 is a pure Dart port of Lottie-android with no native library to compile, ship or fetch. It parses the After Effects JSON itself and draws through Flutter’s own rendering path, so the same file behaves the same way in a browser as it does on a phone. What changes on web is where the bytes come from, how much of them you ship, and how much frame budget the browser gives you.
How the package renders
There is no HtmlElementView and no canvas JavaScript here. The composition is decoded into a LottieComposition, and each frame is drawn by LottieDrawable into a Flutter Canvas, which on the web build becomes whatever renderer the engine is using. The package’s own Flutter Web section is a single line of advice, flutter run -d chrome --web-renderer canvaskit, and a live preview is published at xvrh.github.io/lottie-flutter-web.
That also means the low-level API is available on web exactly as documented: load a composition yourself and paint it from a CustomPainter.
final composition = await AssetLottie('assets/loader.json').load();
CustomPaint(
size: const Size(400, 400),
painter: _LottiePainter(composition, progress),
);
Loading: asset, network, memory
Lottie.asset, Lottie.network and Lottie.memory cover the web-safe paths, and each is backed by a LottieProvider (AssetLottie, NetworkLottie, MemoryLottie) if you would rather load first and render later:
// Bundled with the app: no request, no CORS, no flash of empty space.
Lottie.asset('assets/success.json', width: 200, height: 200, fit: BoxFit.fill);
// Fetched at runtime: needs Access-Control-Allow-Origin on the host.
Lottie.network(
'https://assets.example.com/loading.json',
errorBuilder: (context, error) => const Icon(Icons.refresh),
backgroundLoading: true,
);
Archives work too: pass a .zip or .lottie file to load the animation together with its images, and use the decoder argument for Telegram .tgs (LottieComposition.decodeGZip). errorBuilder and onWarning are your safety net for a 404, a malformed export or a CORS refusal.
The bundle-size reality
The animation file is the payload, so it lands in one of three places. Bundled assets go into the compiled web bundle, which the browser must download before the first frame; a multi-hundred-kilobyte JSON with embedded base64 images is a real cost on a cold load. Network loading moves that cost off the critical path but adds a request and a CORS dependency. Archives split the difference, since images travel beside the JSON rather than inside it.
Fonts are the quiet trap: a composition that names a font family only renders correctly if that font is actually available to the page, so check text layers before shipping and keep onWarning wired up while you do.
Sizing and playback control
LottieBuilder is the widget behind every constructor, and it mirrors Image for layout:
Lottie.asset(
'assets/wave.json',
width: 320,
height: 180,
fit: BoxFit.contain,
animate: true,
repeat: true,
reverse: false,
addRepaintBoundary: true,
renderCache: RenderCache.staticLayout,
);
fit inscribes the animation into the space given, animate and repeat only apply when you have not supplied a controller, and onLoaded is where you copy composition.duration onto your own AnimationController to drive scrubbing. By default the animation plays at the frame rate exported from After Effects, often 10 or 30 FPS, which is a sensible power trade; frameRate: FrameRate.max advances on every rendered frame when you need smoother motion.
Frame budget on the web
Two knobs matter more than micro-optimizations. addRepaintBoundary: true isolates the animation in its own layer so a looping loader does not repaint the widgets around it. And renderCache, added in v3.0, renders frames lazily into an offscreen cache so subsequent runs are much cheaper, trading memory for CPU and GPU time. That combination is the right default for a long-running loop.
Keep an eye on the rest of the page too: a full-bleed 60 FPS composition competes with Flutter’s own layout and paint, so size animations to their content rather than to the window, and test the worst page in your app rather than an empty demo screen.
Accessibility
Reduced motion is a user setting, not a style preference. Flutter exposes it as MediaQuery.of(context).disableAnimations, and a static icon is a perfectly good replacement for a looping loader:
Widget _status() => MediaQuery.of(context).disableAnimations
? const Icon(Icons.check_circle_outline)
: Lottie.asset('assets/success.json');
When to pick something else
If the asset is a static illustration that never animates, inline SVG is smaller and crisper. If it must react to input or app state, Rive is the better runtime, as covered in flutter-lottie-rive-animations, and its web build has its own loading story in flutter-web-rive-integration. Lottie earns its place for decorative, timeline-driven motion: loaders, success ticks, empty states, stickers.
The controller path, and what goes wrong
When you need to scrub or reverse rather than just loop, hand over an AnimationController and the whole Flutter animation toolkit applies on web just as it does on mobile:
late final AnimationController _ctrl = AnimationController(vsync: this);
Lottie.asset(
'assets/onboarding.json',
controller: _ctrl,
onLoaded: (composition) => _ctrl.duration = composition.duration,
frameRate: FrameRate.composition,
);
// From a slider:
void _scrub(double t) => _ctrl.value = t;
The failures that show up first on web, in roughly this order: a 404 or a CORS refusal on Lottie.network, which leaves an empty box unless errorBuilder is set; a composition that names a font the page never loads, so text layers fall back; two animations on one screen both running at FrameRate.max; and a loader placed inside a subtree that rebuilds every frame, which defeats the repaint boundary you just added.
Takeaway: Treat a Lottie on web as a small program you are adding to the page: keep the JSON lean, load it off the critical path, isolate it with a RepaintBoundary, cache repeated frames, and always offer a static frame when the user has asked for reduced motion.