Babel, TypeScript and Prettier on the interpreter
Bundled applications became the interpreter's yardstick: generators run as frames, a large module compiles in linear time, a bundle's object literals are planned again, and a program of 256 or more functions runs with its JIT off, which every bundle measured ran faster without.
Round 28 began on 19 October with the interpreter's per-operation gaps against Bellard's QuickJS and Deno's --jitless mode. Method calls, construction sites, inherited reads and closure reads are now answered by the receiver's shape (d6c38d15); a cached own-property read or write goes through a compact mirror of the object's shape and values (c25ee322); and the lite loop runs hot loops, module top levels and async bodies (49448b77, 2b62543a). A generator step used to enter and leave a nested interpreter loop, about 33 ns for each level of a pipeline or a yield* chain against QuickJS's 12 to 17; it now runs as a frame of the loop that asks for it (732a0855). One shortcut was undone: dropping a discarded assignment's copy whenever the JIT was off made a metered run stop at another step with the JIT off than on. A CI test pins that parity, but every commit skips CI, so it failed unseen until a bisect over the round's 37 commits found it, and the bytecode is the same in both modes again (f6378f2d).
From 20 October the yardstick was bundled applications: Babel, the TypeScript compiler, Prettier, a three.js and lodash bundle, and Vue, Preact and React rendering on the server, each checked against Node's output. Each found a limit sized for small programs. The TypeScript compiler's module compiled in quadratic time until a function's captured bindings were shared and indexed for its nested functions (2e83f5e0), and a function may now declare more locals than the register file holds (b9f618ae). A bundle is one compiler, and one compiler planned at most 256 object-literal sites, so almost every literal in Babel grew its object a property at a time; the limit is now 65,536 sites, bounded by an 8 MiB byte ceiling (130640b0). Past its 4,096th function a bundle's string literals were no longer cached (032d92d0), and one string now serves each literal text (e982f918); a switch over string literals is one table lookup (b63fc659). React's renderer needed TextEncoder and TextDecoder, which now exist (79ddfd28).
Every bundle also ran slower with the JIT on than off: the TypeScript compiler 1.33×, Prettier 1.36×, Babel 1.12×, the three.js and lodash bundle 1.24×, Vue's renderer 1.06× and React's 1.04×. Higher compile thresholds narrowed the gap and never closed it, because a JIT-enabled VM counts every call and meters every instruction even when next to nothing compiles. The benchmark rows the JIT speeds up have at most 56 functions and the large bundles thousands, so a program of 256 or more functions now starts with its JIT off; ZIPP_JIT_LARGE=1 keeps it on (ca511f54). Parsing was not the gap: with each bundle wrapped in a function that is never called, Zipp's front end takes 379, 126 and 178 ms on the TypeScript, Prettier and Babel bundles, and QuickJS's 412, 119 and 190. As the release notes measured it, v0.0.28 takes 5.24 s on Babel's workload where v0.0.27 took 11.31 s interpreted and 13.75 s with its default JIT, 1.17 s on the TypeScript compiler's (2.86 s) and 0.56 s on Prettier's (0.89 s). In a separate interleaved run of the published executables, Bellard's QuickJS took 0.72×, 0.87× and 0.79× of v0.0.28's time on the three.