Rive Integration on Flutter Web: Rendering, Assets, and Performance
Rive on Flutter web: the rive_native WebAssembly runtime, CDN and self-hosted wasm delivery, renderer choice, unchanged state machine API, and asset pitfalls.
Published on • November 12, 2026
AI Assistant

Rive on Flutter web is the same Dart code with a different engine underneath. rive 0.14.11 depends on rive_native 0.1.11, a Flutter plugin that bridges to Rive’s C++ runtime over FFI, and on the web build that bridge is a WebAssembly module fetched at runtime rather than a shared library installed with the app. Everything else, artboards, layouts, data binding and state machines, is unchanged from what you already know from the Rive guide.
What actually runs in the browser
The package’s platform table marks Web as supported by both renderers, so the choice you make in Dart is honored in the browser:
final riveFile = (await File.asset(
'assets/rewards.riv',
riveFactory: Factory.rive, // or Factory.flutter
))!;
Factory.rive selects Rive’s own renderer, Factory.flutter selects Flutter’s (Skia or Impeller on web). All graphics created from that File instance inherit the choice. The legacy rive_common package, currently at 0.4.15 and marked unlisted, is the older shared library from before rive_native replaced it; you should not depend on it directly.
Where the WebAssembly comes from
The setup script does not manage web binaries. On web, rive_native loads its WebAssembly module at runtime from Rive’s CDN on jsDelivr, pinned to the version matching the package. Two consequences follow: the first load of a Rive screen is a network round trip you do not control, and an ad blocker or a strict corporate proxy can deny it.
Self-hosting is a supported escape hatch. Copy the wasm/ and wasm_compatibility/ folders out of the @rive-app/flutter-native-wasm NPM package onto your server, then build with:
flutter run --dart-define=RIVE_NATIVE_WASM_HOST=https://your-host/your-path/
The trailing slash is required, because the runtime appends wasm/rive_native.js or wasm_compatibility/rive_native.js to the host URL you pass.
First paint while the runtime loads
Because the module arrives after the page, there is a visible window where nothing can be drawn. Plan for it rather than letting the layout jump: reserve the artboard’s box with a fixed size, show a lightweight placeholder inside it, and swap to the Rive widget once the file resolves. The same discipline applies to the .riv itself, so a FutureBuilder over your load, or a package-level loader, is worth the dozen lines.
FutureBuilder<File?>(
future: _file, // File.asset('assets/rewards.riv')
builder: (context, snapshot) => switch (snapshot.connectionState) {
ConnectionState.done when snapshot.hasData => RiveWidget(file: snapshot.data!),
_ => const SizedBox.square(dimension: 120, child: CircularProgressIndicator()),
},
);
Asset delivery: bundle or fetch
Bundling a .riv with File.asset is the simplest route and has no CORS surface, but every byte lands in your web bundle, and a library of interactive graphics grows it fast. Loading from a URL moves that weight off the initial download and lets you ship or revise an animation without rebuilding the app, at the cost of a runtime dependency on the host.
| Bundled asset | Network URL | |
|---|---|---|
| First-load cost | In the bundle | Fetched on demand |
| CORS | None | Access-Control-Allow-Origin required |
| Update without redeploy | No | Yes |
| Offline | Works after load | Fails without a cache |
Whichever you pick, serve the file with a real Content-Type, an immutable, versioned URL if you can, and a long cache lifetime, because a .riv that changes on every deploy with a fixed URL is a cache bug waiting to happen. Keep files small: a heavy artboard costs parse time on the main thread before a single frame is drawn.
rive 0.14.11 exposes four static loaders, and on web only two of them are of interest: File.asset for bundled files, and File.url for the network path, which also accepts request headers for signed or authenticated delivery. File.path targets a local filesystem and File.decode takes raw bytes you fetched yourself.
// Bundled: no CORS, ships in the web bundle.
final local = await File.asset('assets/rewards.riv',
riveFactory: Factory.rive);
// Network: off the critical path, needs CORS, supports auth headers.
final remote = await File.url(
'https://cdn.example.com/rive/rewards.v42.riv',
riveFactory: Factory.rive,
headers: const {'Accept': 'application/octet-stream'},
);
if (remote == null) {
// Host refused the file, blocked it, or the bytes were corrupt.
}
Both return File? rather than throwing, so a null result is the normal failure signal and deserves an explicit UI state rather than an unhandled null.
Choosing a renderer on web
The renderer is picked once per File, not per frame, so a screen that mixes both needs two File instances. Factory.rive gives you Rive’s own renderer, which the docs recommend for performance-critical work and which is required for features such as vector feathering; Factory.flutter rides Flutter’s pipeline and is the cheaper switch to flip when something draws wrong. Both are marked supported on Web in the platform table, so this is a rendering decision rather than a platform constraint.
The API does not change
This is the point of rive_native: the Dart you wrote for mobile runs as-is. The widget, the File/artboard selection, StateMachineController, its inputs and listeners, and data binding are all identical in the browser, so state machine authoring stays a design-time concern. What is web-specific is only the delivery of the runtime and the file, which is exactly the two sections above.
Pre-launch checklist and pitfalls
- A hosted
.rivwithout CORS headers fails silently in the console, so test the production origin, notlocalhost. - Missing or wrong
Content-Typeon the.rivwill stop some hosts from serving it correctly. - A CDN that ignores your cache headers means users keep an old animation after a redesign, or never get a new one after a fix.
- Large files delay first paint, since parsing happens before rendering.
- If the Rive renderer misbehaves,
Factory.flutteris a one-line fallback while you triage, and the Impeller advice in the Rive guide applies to the mobile side of the same code.
Takeaway: On web, Rive is your unchanged Dart API plus a runtime that arrives over the network: decide early between the jsDelivr default and self-hosted WASM, reserve space and show a placeholder until the file is ready, load .riv files from a URL with correct CORS and cache headers to keep the bundle lean, and fall back to Factory.flutter if the Rive renderer misbehaves.