Shipping a Flame Game on the Web: Hosting, Wasm, and Browser Gaps
Deploy a Flame game to GitHub Pages, itch.io, and Cloudflare Pages, work around web-only gaps in Flame.device and audio, build with Flutter Wasm, and keep the game at 60 fps.
Published on • November 11, 2026
AI Assistant

Flame treats the web as a first-class platform: because it runs on top of Flutter, its supported platforms are Flutter’s — web, Android, iOS, Windows, macOS, and Linux — and the current release, 1.38.2, is published with all six. Shipping a game to the browser is mostly a static-hosting problem plus a handful of behavior gaps that only show up once it runs there.
Deploy to a static host
flutter build web emits a fully static build/web directory, so any static host works. Flame’s own docs walk through three routes. GitHub Pages is the easiest to automate with the flutter-gh-pages action:
name: Gh-Pages
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
- uses: bluefireteam/flutter-gh-pages@v8
with:
baseHref: /NAME_OF_YOUR_REPOSITORY/
webRenderer: canvaskit
For itch.io, build a web release, delete the <base href="/"> line from index.html, then zip build/web — the directory inside build, not the web/ folder in your project root — and upload the archive. Cloudflare Pages works with the framework preset set to None, a build command that clones Flutter and runs flutter build web --release, and build/web as the output directory.
Fix the web-specific gaps
Three things quietly misbehave on web, and all three are documented in Flame’s platform notes.
Flame.device.setOrientation() and Flame.device.fullScreen() can be called in a web build, but nothing happens — the browser has no such API. Guard them with kIsWeb and treat orientation locking as a mobile-only feature.
Audio has two separate problems. Pre-caching with flame_audio does not work on web because the underlying audioplayers does not support it there; the documented workaround is to GET each file with the http package first, which makes the browser cache it. And browsers refuse to start Web Audio until a user gesture, summarized by MDN as “create or resume context from inside a user gesture”, so unlock it on the first tap:
class MyGame extends FlameGame with TapCallbacks {
bool _audioUnlocked = false;
@override
void onTapUp(TapUpEvent event) {
if (!_audioUnlocked && kIsWeb) {
FlameAudio.play('unlock.mp3', volume: 0); // gesture starts the context
_audioUnlocked = true;
}
super.onTapUp(event);
}
}
Third, ui.decodeImageFromPixels is currently not supported on the web. Flame ships a wrapper — Flame.images.decodeImageFromPixels(...) — whose runAsWeb flag defaults to kIsWeb and decodes through an internal image method instead. For ordinary assets, prefer the standard loaders, Sprite.load, Flame.images.load, or loadSprite, which already work everywhere:
final player = await Sprite.load('player.png');
final image = await Flame.images.load('background.png');
Compile to WebAssembly
flutter run -d chrome --wasm # develop against the Wasm build
flutter build web --wasm # ship it
On Flutter 3.50 and later, the Wasm build needs WasmGC, JS string builtins, and phase-4 exception handling, which means Chrome 137+, Safari 26.2+, and Firefox 147+ at minimum. Browsers below those versions — and Safari and Firefox by default, until you opt in with wasmAllowList — automatically fall back to the JavaScript and CanvasKit build produced by the same command, so a single build serves everyone. Multi-threaded rendering additionally needs Cross-Origin-Opener-Policy: same-origin plus Cross-Origin-Embedder-Policy: credentialless (or require-corp) response headers from your host. One migration to budget for: dart:html and dart:web_gl are deprecated and unsupported when compiling to Wasm, so any interop in your game must use package:web and dart:js_interop — plain flutter build web already prints a dry-run warning listing incompatible imports.
Hold 60 fps in the browser
The frame budget in a browser is the same 16.7 ms as anywhere else, and Flame’s performance guidance applies directly. Never allocate in update or render: a Vector2 or Paint created per frame becomes thousands of garbage-collected objects per second, so hold them as fields. Reuse components with object pooling (ComponentPool), mark hitboxes that never collide as CollisionType.passive so the broadphase skips them, and load sprite atlases instead of dozens of individual images. Reach the game through HasGameReference, and if your physics needs determinism, accumulate delta time into fixed steps rather than stepping with the raw dt:
class Ship extends PositionComponent with HasGameReference<MyGame>, TapCallbacks {
final _velocity = Vector2.zero();
final _paint = Paint()..color = const Color(0xFF38BDF8);
double _accumulator = 0;
static const _step = 1 / 60;
@override
void update(double dt) {
_accumulator += dt;
while (_accumulator >= _step) {
position += _velocity * _step;
_accumulator -= _step;
}
}
@override
void render(Canvas canvas) {
canvas.drawRect(size.toRect(), _paint); // reused, nothing allocated
}
}
Input needs no special casing: Flame’s TapCallbacks and DragCallbacks treat a mouse click and a finger tap identically, so one set of handlers covers desktop and mobile browsers.
Takeaway: Ship a Flame game as static files to GitHub Pages, itch.io, or Cloudflare Pages, guard the calls that no-op on web, unlock audio on the first gesture, build with --wasm for the browsers that support it, and keep allocations out of the game loop.