How SquirrelFish Improved Safari's JavaScript Performance
In June 2008, the WebKit team introduced SquirrelFish, a revamped JavaScript engine that drastically improved the performance of Apple's Safari web browser. This article provides an overview of how SquirrelFish achieved this leap in speed by replacing the browser's older, slower tree-walking interpreter with a highly optimized, register-based bytecode interpreter. By compiling JavaScript into compact bytecode, utilizing direct instruction dispatch, and deferring compilation until necessary, SquirrelFish eliminated massive execution bottlenecks and laid the foundation for modern web application performance.
Eliminating the Tree-Walking Bottleneck
Before SquirrelFish, Safari's JavaScriptCore executed code by parsing scripts into a hierarchical data structure called an Abstract Syntax Tree (AST) and recursively walking through the branches of that tree. This approach was slow because it required countless C++ function calls and pointer lookups just to navigate the structure of the code, resulting in high overhead and poor CPU cache utilization.
SquirrelFish solved this by introducing a compilation step that translated the AST into bytecode—a flat, linear sequence of compact instructions stored in a contiguous block of memory. By executing a linear array of instructions rather than traversing a complex tree, SquirrelFish dramatically reduced memory access times and eliminated the overhead of recursive function calls.
Adopting a Register-Based Virtual Machine
Bytecode interpreters generally fall into two categories: stack-based or register-based. Stack-based machines must constantly push and pop values onto a central stack to perform operations, which requires a high number of instructions and constant memory copying.
SquirrelFish was engineered as a register-based virtual machine. Instead of using a stack, its instructions directly referenced virtual registers—specific slots within a function's local call frame. For example, adding two numbers in SquirrelFish required a single instruction that specified the two source registers and the destination register. This architectural choice significantly reduced the total number of instructions needed to execute a script and minimized the movement of data in memory.
Compile-Time Optimizations
Introducing a bytecode compiler allowed SquirrelFish to analyze and optimize the JavaScript code before a single instruction was executed. The most significant optimization was the resolution of local variables.
In the older engine, finding the value of a local variable required looking up its name in a dictionary-like scope chain at runtime. SquirrelFish's compiler resolved local variables and function arguments directly to specific virtual registers at compile time. During execution, accessing a local variable became an instant array-index lookup rather than a slow string-based search. The compiler also stripped out unnecessary syntax and folded constants, ensuring the interpreter only did essential work.
Direct Threading for Faster Dispatch
Even with efficient bytecode, an interpreter must decode and dispatch
each instruction. Traditional interpreters use a giant loop containing a
switch statement to figure out what each bytecode
instruction does. This causes performance bottlenecks because modern
CPUs struggle to predict where the switch statement will
jump next, leading to stalled processor pipelines.
SquirrelFish bypassed this bottleneck using a technique called direct
threading, utilizing a compiler extension known as "computed goto."
Instead of using integer codes and a central switch
statement, SquirrelFish represented each bytecode instruction as the
actual memory address of the machine code responsible for executing that
operation. At the end of each operation, the engine jumped directly to
the address of the next instruction. This distributed the dispatch
logic, vastly improved CPU branch prediction, and made transitioning
between instructions nearly instantaneous.
Lazy Bytecode Generation
Compiling JavaScript into bytecode takes time, and websites often load large JavaScript libraries while only using a small fraction of their functions. If SquirrelFish compiled every script in its entirety upon loading, webpages would freeze initially.
To keep page load times fast, SquirrelFish implemented lazy bytecode generation. When a webpage loaded, the engine quickly scanned the JavaScript solely to verify that the syntax was valid. It delayed the actual AST generation and bytecode compilation of a specific function until the exact millisecond that the function was called for the first time. Once compiled, the bytecode was cached in memory so subsequent calls to that function ran at full speed. This ensured that Safari never wasted time or memory compiling dead code.