The Dart analysis server, dart2wasm and pub compiled to Wasm: a Flutter IDE that runs client-side with no server

Dart

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's LspAnalysisServer compiled with dart compile wasm -O2: 4.2 MB, runs in a Web Worker, speaks LSP over postMessage. 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 against dart2wasm_platform.dill (8.5 MB) or flutter_platform.dill (16.2 MB).
  • pub: the real VersionSolver from third_party/pkg/pub behind a Source that answers from the pub.dev JSON API. On 5 fixture pubspecs it resolves identically to desktop dart pub get — versions, sha256s, lockfile contents.
  • isomorphic-git over 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. SharedArrayBuffer is 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 Worker with an importScripts blob shim, so module workers die there; the LSP worker is bundled classic and takes its base URL through self.name.
  • The analysis server registers providers dynamically with scheme: 'file', which matches nothing on an opfs: workspace. Diagnostics keep working and everything else silently vanishes. Fix is to strip scheme from 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.wait is banned on a document main thread and a SAB can't cross a BroadcastChannel. 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

20
Loading comments…