The Dart analysis server, dart2wasm and pub compiled to Wasm: a Flutter IDE that runs client-side with no server
https://jamiewest.github.io/dart-browser-ide/ — desktop Chrome/Edge. Static files on GitHub Pages, no backend, no network after first load.
What runs in the tab
- code-oss 1.91.1 (
vscode-web, MIT) as the workbench, booted from static files. analysis_server'sLspAnalysisServercompiled withdart compile wasm -O2: 4.2 MB, runs in a Web Worker, speaks LSP overpostMessage. Boots in ~200 ms with the 472-file SDK source tree seeded. 30 methods register dynamically — completion, hover, quick fixes, rename, references, inlay hints, semantic tokens, call hierarchy, signature help, format.- dart2wasm (
front_end+kernel+dart2wasm) in a second worker, compiling in-memory sources againstdart2wasm_platform.dill(8.5 MB) orflutter_platform.dill(16.2 MB). - pub: the real
VersionSolverfromthird_party/pkg/pubbehind aSourcethat answers from the pub.dev JSON API. On 5 fixture pubspecs it resolves identically to desktopdart pub get— versions, sha256s, lockfile contents. isomorphic-gitover OPFS behind a normal SCM view: stage, commit, diff against HEAD.- Workspace is OPFS (
opfs:/workspace/<project>), shared between the FileSystemProvider and the Wasm workers.
Dart SDK pinned at 3.13.0 stable, Flutter 3.47.0. The framework bundle (~30 MB, fetched only when you create a Flutter project) is mirrored into OPFS and into the LSP worker.
Flutter
Create → pub get resolving sdk: flutter → analyse with dart:ui resolving via sky_engine's _embedder.yaml → dart2wasm build → run on skwasm in a preview tab. Compiled artifacts go into Cache Storage; the service worker serves them to the preview.
Hot restart works and is mechanically trivial: the new build lands in Cache Storage under the paths the preview already requests, so reloading the tab is the restart. The only missing piece was addressing a tab the extension host holds no handle to — same origin, so the preview joins a BroadcastChannel on load and answers ping/reload.
Measurements (Apple Silicon, 8 cores, 16 GB, idle machine)
- Dart compile + run, warm incremental: ~750 ms
- Flutter cold build: 10.5 s (CFE 5.3 s)
- Hot restart: 4.9 s (CFE 0.4–0.55 s). The residual ~4.4 s is dart2wasm codegen, which is not incremental. That is the floor.
- Same builds started while the LSP was still indexing the framework: 114 s and 47 s. Ten times, same machine, same code.
- Renderer RSS: 1.12 GB warm, 2.39 GB peak during a Flutter build (largest single renderer 1.45 GB).
Mechanics worth knowing
- Cross-origin isolation comes from a service worker, because Pages sets no headers.
SharedArrayBufferis only needed for debugger transport; editing, analysis, pub, compile and run all work without isolation. - pub.dev sends
access-control-allow-origin: *on both API and archives, and a CORS-passing fetch also satisfies COEP, so no proxy is involved anywhere. - The extension host replaces
Workerwith animportScriptsblob shim, so module workers die there; the LSP worker is bundled classic and takes its base URL throughself.name. - The analysis server registers providers dynamically with
scheme: 'file', which matches nothing on anopfs:workspace. Diagnostics keep working and everything else silently vanishes. Fix is to stripschemefrom incoming registrations.
Limits
- No git remotes. A tab can't open a TCP socket, so clone/pull/push need a CORS relay, and whoever runs the relay sees the traffic including tokens. Not shipped as a default.
- No breakpoints inside Flutter apps. The debug transport parks the preview's main thread on a synchronous XHR; a synchronous XHR is never dispatched to a service worker, and on a static host there is nothing else to answer it.
Atomics.waitis banned on a document main thread and a SAB can't cross aBroadcastChannel. Dart console debugging is unaffected — breakpoints, call stack, locals, inline values, stepping all work. - No expression evaluation in the debugger.
- Safari: workbench and language server verified running; the compile path is untested there.
- VS Code webview panels are unavailable (they require a per-webview subdomain), which is why the Flutter preview is a tab.
- Desktop only. Memory numbers above.
Posted Aug 21, 2026, 2:28 AM UTC