Repository navigation
Scripting
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.
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 or frame 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. A script reaches every window through its WindowProxy, which stays the same while the frame moves from document to document and shows a window of another origin only what HTML lets it see (document.domain relaxes that between documents that set the same domain), and a message posted between two windows arrives as a task with the sender's WindowProxy as its source and a structured clone of the value, made in the realm that receives it. Every iframe and frame has the initial about:blank document, empty and of its parent's origin, with a window of its own the moment it is in the tree — a frame the parser puts in a <frameset> gets its window as it is inserted — while an iframe in a document no browsing context shows (one a DOMParser made) gets none; one with nothing else to show — no src, an empty one, about:blank — fires load there and then, and one whose src or srcdoc names a document keeps the blank until it navigates there, behind the same WindowProxy, with that document's load alone; the frame's first navigation to a document of the same origin-domain keeps the blank's window itself, with its expandos, listeners and storage, as HTML has it, and any other navigation makes a new window; an iframe or frame a script inserts, or whose src (or an iframe's srcdoc) a script changes, navigates in a task after that script, firing load at the element once; a frame's window is named by its element's name attribute when the frame is made, keeps that name — or what window.name sets — through every document it goes on to, and is never renamed by the attribute again; a window names its frames by those names in tree order, whatever their origin, as every browser does; an <object> or an <embed> decides what it shows in a task after it is inserted, or as the parse ends for the ones the parser made — a document of a type the engine displays (markup, XML and SVG, text, JSON) in a window of its own, named by its name attribute and counted among the page's frames; a picture; an object's fallback content; or an embed's nothing — and fires load, or error at an object whose document cannot be had, as Chrome does; nothing is decided for one inside a <video> or <audio>, inside an object showing something other than its fallback, or under display: none; and a blob: URL URL.createObjectURL made names its Blob's document there; a document the host cannot give the frame leaves it an empty error document of a new opaque origin, and one that is not markup an empty document of its own URL, origin and contentType; a frame's own Location navigates it the same way, a change of the fragment alone firing hashchange instead, and a javascript: URL runs in the frame's document when the document that asked has its origin; the iframe's sandbox attribute is read when the frame navigates — no script without allow-scripts, an opaque origin without allow-same-origin, no navigating the top without allow-top-navigation, and never document.domain; an iframe or frame taken out of its tree closes its frame at once, and every frame inside that one, 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.
A realm is born with names rather than objects. A built-in function is a record — its entry point, its length, the checks it makes of its receiver and its arguments — until a script first asks for it, and is then made in the realm of the object it is defined on, whichever realm asked; a function a page writes gets its prototype object, its name and its length when something looks. The Web's interfaces that everything stands on — nodes, events, the window — are installed with the realm; fourteen groups that are an API of their own (fetch, XMLHttpRequest, ranges, tree walkers, canvas, media, geometry, the mutation and intersection observers, IndexedDB, the CSS object model, Trusted Types, Worker, Origin) and the Streams interfaces are installed when something first touches one: a name on the window, a member the group adds to another interface, or the engine's own need of one of its objects. Nothing a script can list differs for it: the names, their order and their attributes are there from the start, and a frame that is removed installs what it had put off, so its window still answers for every name. A regular expression is read — and its syntax errors thrown — where the standard says, and compiled when it is first run.
A function's body is checked when its script arrives — every syntax error in it is reported before any of the script runs — and then let go of: the function keeps its name, its length and its source text, and its body is parsed again when it is first called, as every shipping engine does. A function in parentheses (which, as a rule, is called at once), one called where it is written, one holding a direct eval or a with, an arrow and a class constructor keep their bodies. YouTube's main script, 10 MB, keeps 64 MB of syntax tree where it kept 174.
What that buys is in the cost of a realm, which every frame and every worker pays: on the machine this is written on a page's realm takes about 0.7 ms and 0.8 MB, and a frame the page keeps about 0.5 ms and 0.6 MB.
A function that runs often is turned into machine code, as every shipping engine's baseline tier does: once its calls and its loops' back-edges together reach 200, its bytecode is compiled in one pass to x86-64 (on Linux and Windows; elsewhere the interpreter runs everything), and a loop goes on in the new code from its next back-edge. The common instructions — local variables, constants, integer arithmetic and comparisons, jumps, a closure's variables, this and the arguments, a property read or written through what its site remembers, an array's element, a global name — are written out directly; anything else, and any case one of those does not cover (a number that is not an integer, an object of a shape the site has not met), is handed to the interpreter's own code for that instruction, so the two cannot disagree. The code keeps the interpreter's frame, so either can take a function over at any instruction, and a call from compiled code to compiled code never leaves it. The memory the code lies in is never writable and executable at once, and a function past 50,000 instructions — a bundle's outer function, which runs once — stays in the interpreter. Since a fine clock makes timing attacks practical against code like this, the clock a page reads (performance.now(), event time stamps) is precise to 0.1 ms, as HR-Time asks of a page that is not cross-origin isolated.
On the eleven small loops of tools/kernels.js compiled code takes two fifths of the interpreter's time (arithmetic in a loop a tenth, a method call half); on YouTube's watch page the scripts take 2–5 % less, since most of that page's time is spent in the browser's own built-in functions.
An object keeps its values in slots — four inside the object itself, a block of its own past that — and what those values are, each property's name, attributes and slot, is its shape. Every object that came by the same properties in the same order from the same prototype has the same shape: the objects a constructor makes, the literals a loop makes, a page's DOM wrappers of one interface. Adding a property, changing one's attributes (as Object.defineProperty and Object.freeze do) or making the object non-extensible moves it to another shape, and the same move from the same shape finds the same shape again, so a page of a million {x, y} objects has one shape for them all. Shapes no object uses any more are freed with the rest of the garbage.
An object that would only ever have a shape of its own keeps its properties as a table it changes in place instead: once a property is deleted from it, past 64 properties, the global object, every object that is another's prototype, a function given properties beyond its own length, name and prototype, and an object holding built-ins not yet made. Nothing a script can see depends on which: the keys come out in the standard's order (indices, then names and symbols as they were added) either way.
On the machine this is written on, a {x, y} object costs about 135 bytes, where it cost 281 before shapes.
Each property read and write in a script, and each name of a global, remembers what it found for the shapes it has met — up to four, then a table the whole engine shares — so that the next run over an object of one of those shapes is a compare of the shape and a load of the value, with no lookup. A remembered answer is dropped the moment what it rested on changes: a property added, deleted or redefined on a prototype the answer came through, a global redefined, a script declaring a let of a global's name. On the eleven small loops the engine is measured on, this takes a third off the time (property reads and writes about five times faster); on YouTube's home page 88 % of named property accesses in the first turn are answered this way — most of the rest are code running for the first time.
-
The tree —
Node,Element,Document,TextandCDATASection,Comment,ProcessingInstruction,DocumentFragment,DocumentType; the HTML element interfaces fromHTMLAnchorElementtoHTMLVideoElementwith their attributes reflected (hrefresolved against the page,valueandcheckedlive from the shell's form state,selectedIndex,options,elements,rows,cells…);querySelectorandquerySelectorAllover the engine's own selector matcher,getElementById,getElementsByClassName,getElementsByTagName,closest,matches;appendChild,insertBefore,replaceChild,removeChild,append,prepend,before,after,replaceWith,cloneNode,insertAdjacentHTML;innerHTMLandouterHTMLthrough a fragment serializer and the fragment parser;textContent;classList,dataset,attributes,getAttributeand the rest;DOMParser,document.implementation.createHTMLDocumentandcreateDocument(an XML document, asnew Document()makes, and only an XML document makes a CDATA section);RangeandStaticRangewith every member — boundary points set and compared,extractContents,cloneContents,deleteContents,insertNode,surroundContents,createContextualFragment— a live range moved by every change to the tree or to a node's data, the parser's included;NodeFilter,TreeWalkerandNodeIteratorwith the standard's algorithms step for step, an iterator's place moved out of whatever is removed, a removal its own filter makes included. Relative URLs —href,src,action,baseURI, whatfetchandXMLHttpRequestare given — resolve against the document base URL, which a<base href>moves. -
Shadow trees and components —
attachShadowandShadowRoot(open or closed, clonable, serializable, slots assigned by name or byslot.assign()),assignedNodes,assignedSlot,slotchange,getRootNode({ composed }); an event's path goes up through the slot a node is assigned to and out of a tree to its host when the event iscomposed, a listener outside a tree is told the host and not the node inside, andcomposedPath()leaves out what a closed tree hides. A<template shadowrootmode>in a page becomes a shadow root as it is parsed;setHTMLUnsafe,Document.parseHTMLUnsafeandgetHTMLread and write the same. Custom elements: a class the page defined is given every element of its name — those already there, those the parser reaches later (constructor, then attributes, thenconnectedCallback, before the next token), and those in shadow trees. -
Modules — static and dynamic
import,import.meta.urlandimport.meta.resolve(), import maps (<script type="importmap">: bare names, prefixes, scopes, a later map adding to an earlier one),HTMLScriptElement.supports(), and modules atblob:anddata:addresses. A script element a script inserts runs after the inserting script, never inside the insertion, and the window's load waits for it. -
Style and geometry —
element.stylereads and writes the style attribute declaration by declaration;getComputedStyleanswers from the computed style;getBoundingClientRect,offsetWidthand 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,
addEventListeneroptions (capture,once,passive),on<type>handlers set as properties or content attributes (areturn falsecancels),Event,CustomEvent,UIEvent,MouseEvent,KeyboardEvent,InputEvent,FocusEvent,WheelEvent,PopStateEvent; the shell firesmousedown,click,keydownandinputat the element under the pointer or in focus, andpreventDefaultkeeps a link from being followed or a control from toggling. -
Binary data —
ArrayBuffer(fixed or resizable,slice,transfer), the typed arrays fromInt8ArraytoFloat64ArraywithFloat16Array, andDataView, all the engine's own (Architecture); on the page sideTextEncoderandTextDecoder(UTF-8 with its streaming and fatal modes, UTF-16, windows-1252),BlobandFilewithslice,text,arrayBufferandbytes,crypto.getRandomValuesover any integer view, andcrypto.subtle.digestover SHA-256, SHA-384 and SHA-512, hashed natively (the rest ofcrypto.subtleis a refused promise until keys are written). -
The network —
fetchwithHeaders,Request,ResponseandFormData(bodies as strings,Blobs, buffers,FormDataas multipart,URLSearchParamsas urlencoded;text,json,arrayBuffer,blob,bytes,formDatato read one),XMLHttpRequestwith its five states, its events andresponseType,AbortController/AbortSignal,MessageChannel/MessagePortandwindow.postMessagebetween windows, each message a structured clone with theArrayBuffers andMessagePorts its transfer list names moved rather than copied. 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 itsAccess-Control-Allow-Originsays so, a request that is not a simple one sends a preflight first,no-corsgets 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(pushStateandreplaceStatemove the URL, and the shell's address field and session history follow — an entry a script pushed is fetched by its address when Back or Forward comes to it;popstate,back(),forward()andgo()are not written yet),navigator,screen,innerWidth,scrollTo,matchMediaover the engine's media queries,localStorageandsessionStorage(in memory),document.cookie(through the session's jar in the window),URLandURLSearchParams,Origin,DOMException,atob/btoa,structuredClone,performance.now,console,alert(logged),IntersectionObserverandResizeObserver(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'sid, or thenameof anembed,form,imgorobject, is a property of the window (one element as itself, several as anHTMLCollectionin 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. -
Workers —
new Worker(url)starts a script in a realm of its own: its own engine, its own heap, its own event loop, and aDedicatedWorkerGlobalScopefor a global —self,postMessage,onmessage,close,importScripts,location,navigator, the timers,fetch,XMLHttpRequest,MessageChannel,URL,Blob, the encoders,crypto,structuredClone— and none of the window: nodocument, nowindow, no DOM. The script comes from the page's own origin, ablob:URL the page made or adata:URL, is a JavaScript MIME type or is not run, and is judged by the page'sworker-src; ablob:ordata:worker runs under its owner's policy, a fetched one under its own response's. A worker and its page share nothing but the messages they post each other — each a structured clone made where it is posted and made again where it arrives,ArrayBuffers andMessagePorts moved — so a port handed to a worker, or by one worker to another through the page, ends up entangled across two heaps and keeps what was queued on it in order. In the window every worker runs on a thread of its own: nothing a worker computes holds the window, whose loop sleeps until a worker has something for its page;terminate()stops a worker in the middle of a loop. A test and a headless run give their workers no threads: the worker takes a turn inside each pump of the page's loop, on the page's clock, as deterministic as the page. A frame's workers run where its page's do.type: "module", shared and service workers, and a worker started by a worker are not written.
A class may extend any of the platform's interfaces — class Store extends EventTarget, class Changed extends Event — and gets an object of its own: an interface's constructor gives what it makes the prototype new.target names.
window.open opens a window of the page's own: the target — _self, _parent and _top, or the name of any frame reachable from here, whether one of this window's own, another frame's, or a window above it in the tree — names a window here and navigates it as though the page itself had navigated it, answering with its proxy, or null when noopener or noreferrer is asked; an empty name, _blank, or a name found nowhere instead answers with a new tab of the page's own — but only while the reader's last click or key is still fresh, for five seconds, and never while the calling script is still running, so the tab appears on the next tick rather than out from under it — and never at all from a sandboxed document whose popups are forbidden, which also answers null. A URL that will not parse throws a SyntaxError.
A script in a frame reaches the rest of what a page can reach against its own document rather than the page's: getBoundingClientRect, offsetWidth and the other box measures come from that frame's own layout and viewport, and scrollTo, scrollX and scrollY read and move that frame's own scrollport, not the page's.
An uncaught exception in any script, listener or timer is an ErrorEvent named error at its global — the window, or a worker's scope — so listeners hear it, and onerror takes its five parts and cancels it by answering true; a worker's that nothing there cancels goes on to its Worker in the page, and one nothing cancels anywhere goes to the console. 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. An error's stack names the functions that were running, innermost first, each with the script it was written in and the line and column it begins at (the engine keeps no position for the call itself).
A script runs once, and only in a document that has a window: a copy of a script that has started has started too, and a script put into a document made by new Document(), createHTMLDocument, DOMParser or a clone never runs; neither is a handler attribute in such a document ever compiled.
What the platform says is a promise is one here, settled the way the engine can honestly settle it rather than answered undefined: document.hasStorageAccess() is true for the page and for a frame on the page's host and false for a third party, whose requestStorageAccess() is refused (the jar is closed to third parties, Security defaults); document.fonts.ready and fonts.load() are settled at once (load() having asked for the faces it names, Styling); play(), an undecodable picture's decode() and requestFullscreen() are rejected; customElements.whenDefined() stays pending.
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, shadow trees and document.write after the parse are not written either, each throwing, absent or saying so on the console rather than pretending.
-
sashfold --render page.html --report out.jsonruns the page's scripts first, their timers on a virtual clock for--script-timemilliseconds (3000 by default);--no-scriptsrenders the markup alone, which is the A of every A/B. The report carries ascriptsobject (run, failed, skipped, external, timers, events, uncaught errors, time), and everyconsole.*line and uncaught error is printed to stderr asconsole.<level>: …. - In a
--scriptreplay,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), andassert-console <text>checks the page's console output. The window and the replay stop a script that runs past ten seconds. -
SASHFOLD_ERROR_STACKS=1in the environment puts an uncaught error's stack lines on the console under its message: where in a page's bundle the thing that failed was called from. -
SASHFOLD_LAZY=0makes everything as a realm is made — every function, every interface, the Streams script, every declared font — which is the other arm of every comparison: the suites give the same results either way; it also parses every function body as the script arrives.SASHFOLD_LAZY_CENSUS=1says at exit what was described, what was asked for after all and what was still made at once, by name;SASHFOLD_REALM_PROFILE=1what each group of interfaces cost each realm;SASHFOLD_REALM_GROUPS=1what each group that is put off provides.realm_cost, a build tool, prices an engine, a realm, a page's realm, a page's frames (--frames N) and what parsing a script keeps (--parse file;--completethen parses every body that was let go of, as if every function were called). -
SASHFOLD_JIT=0runs everything in the interpreter andSASHFOLD_JIT=eagercompiles every function at its first run: with the default, the three arms every suite agrees on test by test (CI runs test262 in all three).SASHFOLD_JIT_THRESHOLDsets the count;SASHFOLD_JIT_DUMP=<dir>writes each compiled function's machine code and bytecode there (objdump -D -b binary -m i386:x86-64); a window's--timingsreport counts the functions compiled and what compiling took. -
sashfold --render page.html --dump-live-dom outwrites the document as its scripts left it toout.htmland each frame's toout-frame-N.html: what a page built is often not what it was sent, and a frame's document is nowhere else to be read. -
tests/test_js_lazy_parse.cppholds a program whose bodies were let go of and then all parsed again to the program an eager parse makes — scopes, tree and every name's resolution — and--sweep <dir>does the same over every script in a directory (test262's, a page's bundles). -
tests/test_bindings.cppdrives a realm the way the shell does, under heap stress,tests/test_workers.cppdrives workers both on the page's thread and on threads of their own (inside wall-clock bands, and clean under ThreadSanitizer), andtests/shell/scripting.scriptdrives the shell with a scripted page.
Getting started
What works today
Measured
Design decisions
Testing and tools