Skip to content

Scripting

codingncaffeine edited this page Sep 13, 2026 · 40 revisions

Pages run their scripts. A JavaScript engine of our own (src/js, see Architecture) is connected to the document through the bindings in src/bindings — the HTML element interfaces, their tags and every attribute reflected from a content attribute are generated from idl/html-elements.idl by tools/gen-bindings.cpp (a WebIDL subset: interfaces with parents, [Tags], [Reflect], [URL], [Default]; the output is committed and a test holds it to the IDL), and the accessors and methods that read live state are written by hand — 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. A type="module" script is fetched with its whole import graph — in CORS mode, with the credentials its crossorigin attribute asks for, each response held to a JavaScript type — and runs after the parse in document order with the deferred classic scripts, or at once when marked async or inserted by a script; an inline module has the document's URL as its base and as its import.meta.url, module code is strict with no this and no document.currentScript, a module named twice is fetched and evaluated once, a failure anywhere in a graph is the element's error event and a console line, and import() from any script resolves against the document on the same module map. Data blocks (JSON, templates, import maps) are skipped by name. <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, one write after another in order, so a script it writes — inline or external — runs when the parser reaches it and sees only what came before it, and a script that a written script writes lands after that script's own end tag; a write after the parse is reported to the console rather than replacing the page, and document.open() answers with the document and replaces nothing.

The document's Content Security Policy (Security defaults) has its say before any of that runs: an inline classic or module script, and an event handler attribute, is held to the governing script-src directive — 'unsafe-inline', its nonce attribute, or a hash of its text in SHA-256, -384 or -512 ('unsafe-hashes' for a handler attribute) — and one refused never runs; an external script's URL is judged with the element's nonce, and under 'strict-dynamic' a script that a trusted script inserted loads where a parser-inserted one needs its nonce; eval, Function and its kin, and a string given to setTimeout, throw an EvalError unless 'unsafe-eval' is allowed; a sandbox directive without allow-scripts runs no script at all, and the page parses with scripting off so its <noscript> content shows. A <meta http-equiv="content-security-policy"> in the head is enforced from the moment it appears, for what follows it. Every refusal is a console line, once.

When the parse is done, readyState moves to interactive, the deferred scripts run, DOMContentLoaded fires on the document, each iframe the parse left in the tree has its document opened in a realm of its own and fires load, readyState moves to complete, and load fires on the window. A frame's realm shares the page's agent — one heap, one microtask queue, one event loop — and has its own global, its own intrinsics and its own document; a script the page holds of the frame's, or the frame of the page's, runs in the realm it was made in, and a message posted between two windows arrives as a task with the sender's window as its source. An iframe a script inserts, or whose src or srcdoc a script changes, navigates in a task after that script, firing load at the iframe once; an iframe taken out of its tree closes its frame at once and ends its realm once the host's entry has returned, so a window a script still holds answers from an empty document rather than from freed memory (Not-written-yet has what a frame does not do yet). 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, which runs the promise reactions and queueMicrotask callbacks queued so far, in one order, until none is left.

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.
  • Binary data — ArrayBuffer (fixed or resizable, slice, transfer), the typed arrays from Int8Array to Float64Array with Float16Array, and DataView, all the engine's own (Architecture); on the page side TextEncoder and TextDecoder (UTF-8 with its streaming and fatal modes, UTF-16, windows-1252), Blob and File with slice, text, arrayBuffer and bytes, and crypto.getRandomValues over any integer view.
  • The network — fetch with Headers, Request, Response and FormData (bodies as strings, Blobs, buffers, FormData as multipart, URLSearchParams as urlencoded; text, json, arrayBuffer, blob, bytes, formData to read one), XMLHttpRequest with its five states, its events and responseType, AbortController/AbortSignal, MessageChannel/MessagePort and window.postMessage to the window itself. A request goes through the shell's own loader with the page's session — cookies only where the request allows them — and the browser's rules apply: a cross-origin response reaches the page only when its Access-Control-Allow-Origin says so, a request that is not a simple one sends a preflight first, no-cors gets an opaque response. The round trip runs on the event loop's next task, synchronously inside it, until the process split; a response never settles a promise or fires an event in the same turn that asked for it.
  • 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); and named access, as HTML has it — an HTML element's id, or the name of an embed, form, img or object, is a property of the window (one element as itself, several as an HTMLCollection in tree order), behind the window's own members, Object.prototype's and every global a script declares, and read from the tree as it stands at each lookup.

An uncaught exception in any script, listener or timer goes to the console with its message and to window.onerror; a promise rejected with no handler by the end of the checkpoint goes to the console once as Uncaught (in promise); the page keeps running.

What is not written

Everything on the engine's own list in Not-written-yet: the v regular-expression flag, lookbehind and property escapes; on the page, import maps, so a bare module specifier fails to resolve. A page whose bundle needs them fails with a syntax error that names the feature, at parse time or at the first call, and the page stands as its markup alone. WebSocket, streams (Response.body is null; a body is read whole), URL.createObjectURL, shadow trees, document.createRange, tree walkers and document.write after the parse are not written either, each throwing or absent rather than pretending.

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> [<times>] moves the pages' clock and runs the timers due, that many times over (so a chain of timers each set by the last runs through), 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