Operation of the Renderer
How the renderer process works
The Renderer process' main job is to convert HTML, CSS and JavaScript into a web page that the user can interact with.
Parsing - DOM Construction
When the renderer process starts to receive HTML data, it begins to build up an internal representation of the page by turning it into a Document Object Model (DOM). The browser also exposes the data structure and API that the web developer can interact with via JavaScript.
Subresource Loading
At the same time as DOM parsing, a "preload scanner" runs concurrently and sends network requests for any referenced resources. Classic examples include the href attributes inside <link> tags or the src attributes into <script> or <img> tags.
When the HTML parser reaches a <script> tag, it has to pause parsing to load, parse ande execute the JavaScript. This is because JavaScript can change the layout of the DOM using document.write or similar functionality, which can change the DOM structure.
JavaScript Execution
JavaScript gets parsed and then turned into an Abstract Syntax Tree (AST). The AST is then used by the interpreter to generate bytecode, which is then run. At this point, the engine is running JavaScript code.
To make the JavaScript run faster, the bytecode can be sent to an optimizing compiler, which makes certain assumptions based on the profiling data it has. It then uses these assumptions to produce highly-optimized machine code. If one of the assumptions ends up being incorrect, the optimizing compiler deoptimizes and goes back to execution via the interpreter.
Based on the JavaScript engine in mind, this execution box at the bottom can vary in appearance. Until 2021, V8 looked exactly like it:
The JavaScript interpreter in V8 is called Ignition, and the optimizing compiler is Turbofan - we'll be meeting it shortly!
Firefox's SpiderMonkey engine has two compilers, one more optimized and powerful than the other:
Meanwhile the JavaScriptCore engine, used by Apple's WebKit, has three optimizing compilers, each providing varying levels of optimization:
To understand what we could use multiple compilers for, we can take a look at the Webkit documentation, which describes when each stage kicks in:
Baseline JIT kicks in for functions that are invoked at least 6 times, or take a loop at least 100 times (or some combination - like 3 invocations with 50 loop iterations total)
DFG JIT kicks in for functions that are invoked at least 60 times, or that took a loop at least 1000 times
FTL JIT kicks in for functions that are invoked thousands of times, or loop tens of thousands of times
The documentation is very clear that these numbers are approximate and based on additional heuristics. Both LLInt and Baseline collect profiling information, so that should function usage reach DFG levels, it has sufficient informaton to perform speculation. The DFG and FTL can both deoptimze back to Baseline if assumptions are broken. Check out the Introduction to Turbofan for more talk on speculation.
Ultimately, different optimiser are used depending on how hot a function is - how many times and how often it is called. Pre-2021 V8 would reach a certain point in execution in Ignition before it loaded up the Turbofan optimizer on a different thread:
JavaScriptCore has a similar operation, but three times:
Why do some engines have different levels of optimization? Mathias Bynens explains it like this:
The reason JavaScript engines have different optimization tiers is because of a fundamental trade-off between generating code quickly like with an interpreter, or generating quick code with an optimizing compiler. It’s a scale, and adding more optimization tiers allows you to make more fine-grained decisions at the cost of additional complexity and overhead. In addition, there’s a trade-off between the optimization level and the memory usage of the generated code. This is why JavaScript engines try to optimize only hot functions.
Last updated
Was this helpful?