Safety review, a rare crash fixed, functions compiled sooner
A safety, memory and speed release with 46 changes. A review of the script engine fixed 38 ways deeply nested code could crash the whole browser (now an error, as in other browsers), five memory-safety bugs, three engine internals a page could reach by name, a key-export leak in Web Crypto, and memory blowups of up to 21 GB. A rare crash where the memory collector could read an object another thread had just freed was found with AddressSanitizer and fixed. Functions are now compiled at their second call instead of their 48th, which cut script time on 11 real pages by about 9 percent while using less memory, and Floati's optimizing compiler now compiles functions that build arrays and objects: a loop pushing 200 numbers runs 2.8 times faster than the reference JavaScript runtime. Lookup tables beside arrays now give dead arrays back after a collection (14 MB left behind -> 0.5 MB). Floati is still ahead of the reference runtime on six of eight standard workloads; reading object properties (1.35x slower) and building and dropping trees of small objects (4.6x slower) are still behind, and this notes page says why.
We went through the engine looking for the kinds of bugs that let one page crash the browser, read memory it should not, or reach engine internals. Thirty-eight ways of nesting code deeply enough to run out of stack used to crash the whole browser with every tab in it; each now throws the RangeError other browsers throw. Five memory-safety bugs were fixed (a spread that moved values out of the object it copied, compiled code thrown away while still in use, a page able to name any byte buffer on its thread, three allocator error paths, and arrays near 4 billion elements asking the system for 25 GB). Page scripts could reach three engine internals by name, worker scripts could read the engine's private slot table, and crypto.subtle.exportKey handed out keys marked non-extractable. All are fixed, each with a test that fails if the fix is removed.
Deep-nesting shapes that crashed the browser38 -> 0 (now RangeError)Memory-safety bugs fixed5 in the review + 1 rare crash (below)Engine internals a page could reach3 -> 020,000 classes in a chain5 GB -> 44 MB100,000 binds of one function21 GB -> 117 MB
A rare crash found and fixed
Floati frees dead objects on a second thread so pages do not pause. Very rarely the memory collector on the page's thread took a weak handle to an object at the same moment the second thread freed it, and later read freed memory: a crash maybe once in hundreds of heavy test runs. Earlier tools could not catch it; running a stress test under AddressSanitizer a few hundred times did, with the exact place it happened. The collector now waits for the second thread's work before it looks at such an object. Stress runs: 3 failures in 69 before, 0 in 250 after.
Stress runs failing (AddressSanitizer)3 of 69 -> 0 of 250
Functions compiled at their second call
Floati used to compile a function to its fast form after 48 calls; now it does at the second, after making the step itself cheaper (a large framework's code takes 56 ms to prepare instead of 470) and keeping only the bookkeeping each operation needs, so memory went down, not up. Script time on 11 real pages fell about 9 percent with no page slower. Floati's optimizing compiler also learned to compile functions that build arrays: it used to refuse any function that allocated anything. A loop pushing 200 numbers now runs 2.8 times faster than the reference JavaScript runtime.
Script time, 11 real pages-9.5% (6 faster, none slower)A loop pushing 200 numbers1.1 -> 0.3 ms (reference runtime 0.85)Peak memorybelow 0.1.383
Still behind on two workloads
On the eight standard workloads (median of three runs, best of five timings each, same Mac) Floati is ahead on six: numeric 2.8 vs 3.4 ms, recursion 1.6 vs 2.1, bitwise hash 3.7 vs 4.6, string building 1.0 vs 1.8, Map 12.3 vs 13.5, array callbacks 2.1 vs 2.3. Reading the properties of 2,000 objects takes 3.6 ms against the reference runtime's 2.7 (1.35x; it was 4.0 in 0.1.383), and building and dropping a tree of small objects 3.8 ms against 0.8 (4.6x). We measured where the time goes before choosing what to build. For the tree, about 60 percent is freeing each dead object one by one, and the rest is building it: functions that allocate could not use Floati's best compiler at all until this release. This release starts on both: the optimizing compiler now builds arrays and number objects itself (a push loop is 2.8x faster than the reference runtime), and the next steps are object literals with any values, small calls inlined, and a young-object model in which short-lived objects die without work per object. Object properties need smaller objects with their fields in place, which is the same memory-model work.
Object properties3.6 ms vs 2.7 (1.35x behind; 4.0 in 0.1.383)Build and drop a tree of small objects3.8 ms vs 0.8 (4.6x behind)Workloads ahead of the reference runtime6 of 8
What changed 46 changes
Memory & reading
FIX (memory safety): copying an object with spread could move values out of the source object's storage, leaving it reading freed memory. The source now keeps its values.
Memory & reading
FIX (memory safety): a compiled function could be thrown away while a faster caller still pointed into it. Compiled code is now discarded all at once or not at all.
Privacy & Shields
FIX (memory safety): a page could name any byte buffer on its thread (another frame's, WebAssembly memory, key bytes) by writing one internal field on an object. Only the engine's own buffer handles can name a buffer now, and a buffer slot whose generation counter wraps is retired instead of reused.
Memory & reading
FIX (memory safety): the memory allocator kept a failed in-place growth as if it had worked, marked a failed reset as zeroed memory, and unwound a failed commit as committed pages. Each error path now leaves the allocator exactly as before.
Memory & reading
FIX: arrays with a length near 4 billion (`a.length = 2**32 - 1`, `a[4e9] = 1`, `new Array(4e9)`) asked the system for 25-32 GB and the tab died. Growth past 50 million elements is now a RangeError, a length that is not a whole 32-bit number is a RangeError as the standard says, and setting `length` calls valueOf.
JavaScript & JIT
FIX: 38 kinds of deeply nested script (long expression chains, deep JSON, recursion through built-ins) crashed the whole browser process with every tab in it. They now throw RangeError "Maximum call stack size exceeded", as the reference engines do.
Rendering
The parser checks nesting depth for statements, expressions, patterns and `new` chains against what the later passes need, and parallel parsing stays within the running thread's stack.
JavaScript & JIT
Stack guards in JSON.parse, JSON.stringify (fast path, indented output, reviver), Array.prototype.flat, structuredClone, the regular expression parser, nested template literals and calls between built-ins (join calling toString, bound functions, yield*).
Memory & reading
Freeing a structure nested deeper than the stack can follow (a Map holding a Map holding ... ) parks the rest and frees it from a shallow frame instead of crashing.
JavaScript & JIT
The browser's script thread gets a 64 MB stack (was 2 MB), so legitimate deep recursion goes as far as in other browsers.
Privacy & Shields
FIX (privacy): a page variable named like one of the engine's internal globals reached the engine's own object: every object's private slots, an eval that ignores the page's Content Security Policy, and the code every for-in loop compiles to. Page names can no longer reach engine names.
Privacy & Shields
FIX (privacy): worker and service-worker scripts could read the engine's private slot table through `self`. Their start-up code is now engine code the worker cannot see.
Privacy & Shields
FIX (privacy): crypto.subtle.exportKey handed out the bytes of keys made non-extractable, and the extractable check read a value a page could replace. The flag is now kept where pages cannot change it.
Memory & reading
A chain of 20,000 classes each extending the last took 5 GB (every class copied its whole ancestry). It now takes 44 MB: each class links to its parent.
Memory & reading
Binding a function 100,000 times took 21 GB for the names (`bound bound bound ...`). It now takes 117 MB, and binding a bound function binds its target directly.
JavaScript & JIT
typeof on a proxy walked the whole chain of proxies it wraps (100,000 proxies: 77 s). It is now recorded when the proxy is made (0.05 s).
Rendering
Nested template literals were copied at every level (quadratic); nesting is capped at 512 levels.
Rendering
FIX: `super.x` works in classes without `extends`, in instance and static methods.
Rendering
FIX: syntax errors inside eval and new Function are SyntaxErrors (they were plain Errors).
Rendering
FIX: two statements on one line without a semicolon (`a b`) are a SyntaxError, an unterminated template literal is a SyntaxError, and a `//` comment ends at the less common line terminators too. Language conformance tests: 17,711 -> 17,758 passing.
JavaScript & JIT
Functions get compiled at their 2nd call instead of their 48th, with planning made cheaper (a large framework's code: ~470 -> ~56 ms to plan). Script time on 11 real pages: 8.3 -> 7.6 s (-9.5%, 6 faster, none slower).
Memory & reading
Compiled functions keep bookkeeping only for the operations that use it (2.67 MB -> 74 KB for a page's first 200 functions), so compiling earlier costs no memory: peak memory is now below 0.1.383, and script time on 11 real pages is 8.8% lower than 0.1.383.
JavaScript & JIT
FIX: compiled code read a top-level let / const / class before its declaration as undefined; it now throws ReferenceError like the interpreter. Found by compiling functions earlier.
JavaScript & JIT
FIX: a string past the length limit made in compiled code now throws a RangeError a try/catch can take.
JavaScript & JIT
FIX: error.stack lost every frame outward from the first compiled function. Compiled calls are now recorded where stack walks look.
Privacy & Shields
FIX: the XSS risk map in DevTools put writes made through a compiled function at line 0; it now shows the real call site.
DevTools
FIX: the DevTools profiler billed timer and event callbacks that run compiled to "(program)" with no parent; they now show under their real caller.
Memory & reading
Typed-array element reads and writes and `.length` in compiled code skip the by-name lookups: each index site remembers its view (a compiled typed-array helper ~41% faster).
Memory & reading
The cycle collector stores a re-severed array once (one loop had 5,038 entries for 34 arrays), and its capped walk keeps going while candidates wait.
JavaScript & JIT
Interpreted calls reuse their argument lists instead of allocating one per call (call-heavy interpreted loop -6.5%).
Memory & reading
Objects are 8 bytes smaller (the lock state moved into the header word): a 3-field object 88 -> 80 bytes.
JavaScript & JIT
A function that runs once and has a for...of (every for...of is a try) now moves from the interpreter into compiled code mid-loop: a once-run loop calling a small function 142 -> 18 ns per call.
Tabs & interface
The release check now finds a Python with the numeric library it needs (0.1.383's check silently skipped two stages after a system update) and reports a lost connection to the Windows build machine as not run, not as a failure.
Rendering
The tier test matrix marks the kill switches that restore documented old answers as expected differences, so a real regression stands out.
Memory & reading
New instruments: a slot census (what object fields hold on real pages: doubles under 1%, objects about 2 fields), a young-death census (69-100% of objects on 13 real pages die within 1 MB of allocation), and allocator debug modes that poison or fence freed memory.
Memory & reading
Groundwork for the next memory model: object boxes get their own allocator size classes, every array write goes through one checked interface, array boxes come from the same classes, and the allocator can mark pages young (all off by default).
Memory & reading
FIX (memory safety): a rare write to freed memory. When an object's count dropped to 1 while the background freeing thread was freeing the structure holding its last reference, the cycle collector took a weak handle to it, and the next collection read freed memory. Found with AddressSanitizer (3 failures in 69 stress runs -> 0 in 250).
Memory & reading
Compiled object and array literals stay on their fast path on fresh memory pages: the allocator now lays out the next blocks of a fresh page for compiled code (building trees that stay alive: -20%).
Memory & reading
Freeing a tree reads the background freeing thread's state once instead of four times (small-object tree row 3.7 -> 3.6 ms).
JavaScript & JIT
Floati's optimizing compiler now takes functions that make an array and push numbers onto it (it refused any function that allocated): a push loop 1.1 -> 0.6 ms.
JavaScript & JIT
Pushes onto that array happen in place in the compiled code: a loop pushing 200 numbers now runs in 0.3 ms per 1000 calls, 2.8x faster than the reference runtime (0.85).
JavaScript & JIT
The optimizing compiler also builds object literals of numbers pushed onto such an array, allocating them in place (object push loop 2.35 -> 1.65 ms).
JavaScript & JIT
FIX: `{ __proto__: 5 }` and `o.__proto__ = 'x'` stored the number or string as the object's prototype, so its properties came from Number.prototype or String.prototype. Values that are not an object or null are now ignored, as the standard says.
Memory & reading
FIX (memory): 40,000 short-lived arrays with holes or extra properties left ~14 MB behind after they were collected, because lookup tables beside the arrays kept each one's box until a later clean-up; one of those tables also re-scanned itself on every insert (quadratic). The tables are now cleaned after each collection: 14 MB -> 0.5 MB, and that test runs 5x faster.
Memory & reading
FIX: the memory test for those tables passed only because another test ran at the same moment and inflated its starting measurement; tests that measure the whole heap now run one at a time.
Memory & reading
FIX: an allocator test touched memory Windows had only reserved, failing the Windows test stage; it now runs only where it applies.