-
-
Notifications
You must be signed in to change notification settings - Fork 159
Description
Summary
This RFC proposes adding a Headless JS Context to WebF — a lightweight JavaScript execution mode that provides the QuickJS runtime + Web APIs + module system without initializing the DOM/CSS rendering pipeline.
This would enable Flutter apps to leverage the massive npm ecosystem of pure-logic JS/TS libraries (authentication, real-time data, IoT protocols, cryptography, etc.) without the overhead of a full browser environment.
Problem
1. DOM overhead for non-UI libraries
Currently, every WebFController unconditionally initializes:
Document+Window+ all HTML element constructors- CSSOM infrastructure (style resolution, layout algorithms)
- Rendering command buffer (
pendingUICommands)
This is unnecessary when running libraries like supabase-js or mqtt.js that never touch the DOM. Each context carries the full memory footprint of a browser page even when only fetch + WebSocket + localStorage are needed.
2. No official lightweight alternative
Mercury (mercuryjs) was the previous answer for headless JS execution, but it has been deprecated. There is currently no official way to run JS libraries in a Flutter app via WebF without DOM overhead.
3. preRendering mode is a workaround, not a solution
WebFController with preRendering mode can execute JS without a mounted widget, but:
- Still initializes the full DOM tree (
initBridge()→allocateNewPage()→ Document/Window/Elements) clientWidth/clientHeight/getBoundingClientRectreturn 0 — some libraries' feature detection breaksevaluateJavaScripts()returnsFuture<void>— no way to get return values from JS- No JS→Dart event push channel (only Dart→JS via
dispatchEvent)
Use Cases
Pure-logic JS/TS libraries that would benefit from headless execution:
| Library | Purpose | Required Web APIs |
|---|---|---|
@supabase/supabase-js |
Auth, Database, Realtime, Storage | fetch, WebSocket, localStorage |
firebase/auth + firebase/firestore |
Authentication + Database | fetch, XMLHttpRequest, localStorage |
@aws-sdk/* |
AWS services SDK | fetch |
mqtt.js |
IoT MQTT protocol | WebSocket |
socket.io-client |
Real-time communication | WebSocket, XMLHttpRequest |
graphql-request / urql (core) |
GraphQL clients | fetch |
zod / ajv |
Data validation | None (pure computation) |
crypto-js / tweetnacl / jose |
Cryptography, JWT | None (or crypto.subtle) |
protobufjs |
Protocol Buffers encoding | None |
@tanstack/query-core |
Async state management | None |
rxjs |
Reactive programming | None |
i18next |
Internationalization | None |
date-fns / dayjs |
Date manipulation | None |
These libraries represent a massive portion of the npm ecosystem — all pure logic, no DOM needed.
Proposal
Core: Headless JS Context
A new mode for WebFController (or a new lightweight controller class) that:
- Skips DOM initialization — no Document, Window, Element constructors, no CSSOM
- Retains Web API polyfills — fetch, WebSocket, localStorage, sessionStorage, timers (setTimeout/setInterval/requestAnimationFrame), URL/URLSearchParams, TextEncoder/TextDecoder, AbortController, EventSource, crypto.randomUUID()
- Retains the module system —
defineModule/invokeModuleAsync/addWebfModuleListenerwork identically to full contexts, enabling headless contexts to call the same Dart modules as UI contexts - Runs on a separate
DedicatedThread— isolated from UI contexts to prevent blocking
Enhancement 1: evaluateJavaScripts with return values
Current:
Future<void> evaluateJavaScripts(String code);Proposed:
Future<dynamic> evaluateJavaScripts(String code); // returns the JS expression resultThis is critical for Dart→JS function calls. Without return values, every interaction must be routed through the module callback system, which is awkward for simple queries like:
// What we want:
final user = await headlessContext.evaluateJavaScripts(
'supabase.auth.getUser()'
);
// What we have to do today:
// 1. Define a module just to receive the result
// 2. Call evaluateJavaScripts to trigger the query
// 3. Have JS call invokeModuleAsync to push the result back
// 4. Collect it in Dart via a CompleterEnhancement 2: JS→Dart event channel
Current architecture supports:
- ✅ JS → Dart calls (
invokeModuleAsync) - ✅ Dart → JS events (
dispatchEvent) - ❌ JS → Dart events (missing)
For real-time libraries (Supabase Realtime, MQTT, Socket.IO), JS receives streaming data via WebSocket callbacks. There is no way to push these events to Dart without polling via module calls.
Proposed: A postMessage-style API where JS can emit events that Dart listens to:
// JS side
webf.postMessage('channel', { type: 'new_message', data: {...} });// Dart side
headlessContext.onMessage('channel', (data) {
// Update Flutter UI with real-time data
});Suggested Dart API
// Create a headless context (no DOM, no rendering)
final context = await WebFController.createHeadless(
bundle: WebFBundle.fromUrl('assets:///supabase_bundle.js'),
thread: DedicatedThread(), // isolated thread
onError: (String error) => talker.error('[Supabase] $error'),
);
// Call JS functions and get return values
final result = await context.evaluate('supabase.auth.getSession()');
// Listen to JS-pushed events
context.onMessage('realtime', (data) {
setState(() => messages.add(data));
});
// Existing module system still works
// JS can call: webf.invokeModuleAsync('SecureStorage', 'get', 'token')
// Lifecycle management
context.dispose();Technical Considerations
Bundle Loading
Headless contexts should accept WebFBundle (same as today). A Vite-built IIFE bundle wrapping supabase-js works without HTML. The WebFBundle.fromUrl() and .fromContent() APIs are sufficient.
Node.js API Polyfills
Many npm browser builds still reference:
process.env.NODE_ENV— should be handled by the build tool (definein Vite)crypto.randomUUID()— would be valuable to support in WebF's crypto bindingBuffer— can be polyfilled via npmbufferpackage in the bundle
Memory & Isolation
Each headless context should have its own QuickJS heap (same as today's context isolation). No shared globals between contexts. Data sharing goes through Dart-side module calls or shared storage (SQLite, localStorage).
Error Handling
Headless contexts must wire onJSError — without a visible UI, silent error disposal (current default when onJSError is null) is dangerous. Consider making onError a required parameter for headless contexts.
Alternatives Considered
| Approach | Pros | Cons |
|---|---|---|
| Mercury (deprecated) | Lightweight, Apache-2.0 | Deprecated, no module system integration |
| preRendering mode | Works today | Full DOM overhead, no return values, no JS→Dart events |
| flutter_js / jsengine | Third-party QuickJS bindings | No WebF module system, no fetch/WebSocket polyfills, separate ecosystem |
| InAppWebView evaluateJS | Full browser APIs | Heavy WebView overhead, platform-specific, no module system |
Impact
This feature would position WebF as not just a web rendering engine, but a comprehensive JS runtime platform for Flutter — enabling developers to tap into the entire npm ecosystem for both UI and non-UI use cases. The combination of headless contexts (for logic) + full contexts (for UI) + shared module system (for native capabilities) would be uniquely powerful.