This document describes how JavaScript + web APIs plug into FastRender’s existing staged renderer. It is intended to be a contributor-facing mental model, not an implementation log.
Workstreams/spec anchors:
- JS workstreams:
js_engine.md,js_dom.md,js_web_apis.md,js_html_integration.md - ecma-rs ownership:
docs/ecma_rs_ownership.md - Renderer pipeline overview:
docs/architecture.md - Conformance matrix (repo reality):
docs/conformance.md - Runtime/container map (which public types have JS + event loop + live DOM mutations):
docs/runtime_stacks.md
Today the renderer is roughly:
fetch → parse HTML → parse CSS → cascade/compute → box tree → layout → paint
With JS enabled, the pipeline is still staged, but the document can be mutated:
- JS can run during parsing (
<script>processing model) and after parsing (event loop tasks). - JS can mutate DOM, attributes, and (eventually) stylesheets; those mutations must trigger:
- style invalidation,
- layout invalidation,
- paint invalidation.
The first correct integration point is usually:
- Run a task (e.g. a script, a timer callback, an async script “ready” task)
- Run a microtask checkpoint
- If the document is dirty, do a full re-style/re-layout/repaint before the next “frame”
Incremental invalidation can come later; correctness comes first.
FastRender’s role is to provide the host environment that ECMAScript expects:
- a realm + global object (
Window-shaped global), - host hooks for module loading (static imports + dynamic
import()), - task scheduling (timers, async scripts, networking integration),
- Web IDL-backed DOM and web APIs.
The JavaScript language implementation itself lives in vendor/ecma-rs/ (per the workstream).
JavaScript execution must follow the HTML Standard’s script processing model. The most important early behaviors to preserve:
- Parser-inserted classic scripts: pause parsing, fetch/prepare the script, run it, then resume parsing.
deferclassic scripts: run after parsing completes (before “document ready” milestones).asyncclassic scripts: run when ready, independent of parser progress (scheduled as tasks).- Module scripts: supported (
type="module") whenJsExecutionOptions.supports_module_scriptsis enabled (opt-in for hostile-input safety; implemented by the productionBrowserTab+VmJsBrowserTabExecutorembedding):- static import graphs,
- dynamic
import()from both classic and module scripts (honors import maps), - top-level
await(module evaluation may complete asynchronously; completion is surfaced back into the HTML event loop to unblock ordered module queues).
- Import maps: parsing + merge/register/resolve algorithms exist in
src/js/import_maps/. Inline<script type="importmap">is supported in bothBrowserTabandfetch_and_render --js, and module specifier resolution goes through the activeImportMapState; seedocs/import_maps.md.
Correctness requirements that fall out of this:
- scripts must be able to observe/modify the partially-built DOM during parsing,
- running a script must be followed by a microtask checkpoint,
- script execution must interact with the event loop/task queues (async scripts, network, timers).
FastRender needs an HTML-shaped event loop model:
- one or more task queues (start with a single queue; split by “task source” later),
- a microtask queue for Promise jobs /
queueMicrotask, - a timer queue for
setTimeout/setInterval(driven by a host-controlled clock so tests can be deterministic), - separate callback queues for
requestAnimationFrameandrequestIdleCallback(driven by the embedding’s “frame/tick” loop; seedocs/live_rendering_loop.md), - explicit microtask checkpoint points (not “whenever convenient”).
In the vm-js embedding, Promise jobs enter the host through vm_js::VmHostHooks
(vendor/ecma-rs/vm-js/src/jobs.rs). FastRender’s VmJsEventLoopHooks implementation
(src/js/vmjs/window_timers.rs) routes each vm_js::Job into the host-owned
EventLoop microtask queue so Promise reactions run during HTML microtask checkpoints.
Minimum semantics to preserve early:
- after running any script or task callback, run a microtask checkpoint until the microtask queue is empty,
- microtasks can schedule more microtasks (drain until stable),
- tasks scheduled during a task run should not run until the next task turn.
Hand-authoring JS bindings does not scale. The binding surface should be Web IDL-shaped:
- Parse IDL from spec sources (e.g. WHATWG DOM/HTML/WebIDL).
- Generate deterministic Rust glue:
- JS-visible prototype chains and property descriptors,
- argument conversions and overload resolution,
- exception mapping (Web IDL exceptions → JS throws),
- exposure rules (
[Exposed=Window], etc.).
Contributor workflow details (codegen, determinism, committed snapshot): see
docs/webidl_bindings.md.
For the consolidated WebIDL crate layout and ownership boundaries (what belongs in vendor/ecma-rs/
vs src/js/), see docs/webidl_stack.md.
The goal is that adding a new web API looks like:
- pick the spec IDL + algorithms,
- implement host-side behavior in Rust,
- regenerate bindings,
- add targeted tests (WPT subset / fixtures).
The initial “web platform” primitives that unlock real sites:
setTimeout/setInterval(tasks scheduled into the event loop),- Promise job queue integration with the microtask queue,
queueMicrotask.
Implementation constraint: timers must be deterministic under tests (time should be controlled by the harness, not wall-clock time).
FastRender already needs a network stack for document and subresource loading. JS support adds a second layer:
- expose WHATWG URL parsing/serialization via
URL/URLSearchParams, - expose Fetch (
fetch(),Request,Response,Headers) incrementally on top of the existing loader.
Fetch is a large spec; the goal is to stay spec-shaped and grow coverage, not to “fake it” with ad-hoc behavior.
These are requirements, not optimizations:
- Interrupts/budgets: JS execution must be interruptible so
while(true){}cannot hang the process. Budgets can be fuel/"tick" based, wall-time based, or both, but must be enforced predictably. - Bounded allocations: DOM wrappers, strings, arrays/typed arrays, and caches must be bounded or governed by the renderer’s resource limits. Avoid unbounded growth from hostile scripts.
- Deterministic tests: conformance tests must be offline and stable; event loop time and scheduling must be controllable.
When tradeoffs are required, prefer a smaller, correct, budgeted subset over an unbounded “mostly works” implementation.
FastRender’s windowed browser app (see docs/browser_ui.md and
src/bin/browser.rs) is the primary interactive surface for live
rendering.
JavaScript execution in the windowed UI is experimental and is currently enabled by default (there
is no stable CLI toggle to disable it yet). In --headless-smoke mode,
browser --headless-smoke --js selects a vm-js BrowserTab smoke test (this is what the
browser --js flag currently controls).
- Author
<script>elements execute during navigation/rendering (HTML script processing model), so JS-driven pages can build/modify the DOM before the first “visual” frame. - DOM mutations can invalidate style/layout/paint and trigger repaints (today this is generally a full rerender, not an incremental damaged-rect compositor).
- Time-based behavior is driven by an explicit UI tick loop:
- Each rendered frame reports
RenderedFrame.next_tick: Option<Duration>(inWorkerToUi::FrameReady). - While
next_tickisSome(delay), the UI sendsUiToWorker::Tick { tab_id, delta }messages to advance time-based effects (CSS animations/transitions, animated images, JS timers, andrequestAnimationFrame) and repaint when needed.deltais the elapsed time since the previous tick delivered for the tab.
- Each rendered frame reports
For the message-level protocol and scheduling details, see the “Tick loop” section in
docs/browser_ui.md. For the library embedding surface and JS execution budgets,
see docs/js_embedding.md.