Skip to content

Scripting

codingncaffeine edited this page Sep 5, 2026 · 40 revisions

Pages run their scripts. A JavaScript engine of our own (src/js, see Architecture) is connected to the document through hand-written bindings (src/bindings), and every host — the window, --render, the WPT runner and the shell's --script replay — drives the same realm the same way.

How a script runs

The parser hands each <script> element over as its end tag goes by, with the parser paused, as the HTML specification asks: an inline classic script runs at once, an external one is fetched through the page's own session and runs at once, and one marked defer or async runs after the parse, in order, before DOMContentLoaded. type="module" scripts and data blocks (JSON, templates, import maps) are skipped by name — modules are not written. <noscript> content is raw text, since scripting is on. A <script> element a script inserts into the document runs; the scripts inside markup set through innerHTML never do, as the specification says. document.write while the parser runs puts its text at the insertion point.

When the parse is done, readyState moves to interactive, the deferred scripts run, DOMContentLoaded fires on the document, readyState moves to complete, and load fires on the window. Timers then run on the host's clock — wall time in the window, a virtual clock in the renderer and the runners — and every entry from the host into script ends with a microtask checkpoint.

What a page can reach

  • The tree — Node, Element, Document, Text, Comment, DocumentFragment, DocumentType; the HTML element interfaces from HTMLAnchorElement to HTMLVideoElement with their attributes reflected (href resolved against the page, value and checked live from the shell's form state, selectedIndex, options, elements, rows, cells…); querySelector and querySelectorAll over the engine's own selector matcher, getElementById, getElementsByClassName, getElementsByTagName, closest, matches; appendChild, insertBefore, replaceChild, removeChild, append, prepend, before, after, replaceWith, cloneNode, insertAdjacentHTML; innerHTML and outerHTML through a fragment serializer and the fragment parser; textContent; classList, dataset, attributes, getAttribute and the rest; DOMParser, document.implementation.createHTMLDocument.
  • Style and geometry — element.style reads and writes the style attribute declaration by declaration; getComputedStyle answers from the computed style; getBoundingClientRect, offsetWidth and the other box measures come from a layout the host runs on demand, so a script that measures and then sets a width gets the number the page would have.
  • Events — the dispatch algorithm with capture, target and bubble phases, addEventListener options (capture, once, passive), on<type> handlers set as properties or content attributes (a return false cancels), Event, CustomEvent, UIEvent, MouseEvent, KeyboardEvent, InputEvent, FocusEvent, WheelEvent, PopStateEvent; the shell fires mousedown, click, keydown and input at the element under the pointer or in focus, and preventDefault keeps a link from being followed or a control from toggling.
  • The window — setTimeout, setInterval, requestAnimationFrame, requestIdleCallback, queueMicrotask; location (assigning it navigates), history (pushState moves the URL), navigator, screen, innerWidth, scrollTo, matchMedia over the engine's media queries, localStorage and sessionStorage (in memory), document.cookie (through the session's jar in the window), URL and URLSearchParams, DOMException, atob/btoa, structuredClone, performance.now, console, alert (logged), IntersectionObserver and ResizeObserver (each observed target reported once, in view), MutationObserver (constructible; records are not written), customElements.define (recorded, not upgraded).

An uncaught exception in any script, listener or timer goes to the console with its message and to window.onerror; the page keeps running.

What is not written

Everything on the engine's own list in Not-written-yet: private class members, generators, async functions and promises, modules, Map and Set, typed arrays. A page whose bundle needs them fails at parse time with a syntax error that names the feature, and the page stands as its markup alone. fetch, XMLHttpRequest, WebSocket, shadow trees, document.createRange, tree walkers and document.write after the parse are not written either, each throwing or absent rather than pretending. Named access on the window — a bare foo for the element with id="foo" — is not written.

Instruments

  • sashfold --render page.html --report out.json runs the page's scripts first, their timers on a virtual clock for --script-time milliseconds (3000 by default); --no-scripts renders the markup alone, which is the A of every A/B. The report carries a scripts object (run, failed, skipped, external, timers, events, uncaught errors, time), and every console.* line and uncaught error is printed to stderr as console.<level>: ….
  • In a --script replay, advance <ms> moves the pages' clock and runs the timers due, and assert-console <text> checks the page's console output. The window and the replay stop a script that runs past ten seconds.
  • tests/test_bindings.cpp drives a realm the way the shell does, under heap stress, and tests/shell/scripting.script drives the shell with a scripted page.

Clone this wiki locally