05_Working Mechanism of JavaScript in Modern browser - V8 engine concept
Lets explore working of modern browser:
"JavaScript engine is a computer program that executes JavaScript (JS) code. The first JavaScript engines were mere interpreters, but all relevant modern engines utilize just-in-time compilation for improved performance"
- Js engines focus lot on performance optimizations for ES6 and ES6 next features. JIT compilation generate machine code (AOT), Ahead of TIme , that means, We are not first compiling Ahead of time and running code . Actually we are mixing this together.
" In simple term , We are compiling the source code Just IN Time We need. Compilation and Execution process goes at same time , there is feed back going back and forth to speed up execution"
- The first thing that happens within the JavaScript engine is parsing of our code by parser. A parser basically reads our code line by line and checks it for errors and stops execution if any, if the code is free of syntactic errors, the parser produces a data structure called abstract syntax tree, which is then translated to machine code and thus results to output.
JavaScript parsing
- let’s see how an AST gets built. We have a simple JavaScript function as an example:
function foo(x){
if(x > 10){
var a = 2;
return a * x;
}
return x + 10;
}
foo(3);
The parser will produce the following AST.
Note that for visualization purposes, this is a simplified version of what the parser would produce. The actual AST is much more complex. The idea here, however, is to get a feel for what would be the first thing that would happen to the source code before it gets executed. If you want to see what the actual AST looks like, you can check AST Explorer. It’s an online tool in which you pass some JavaScript and it outputs the AST for that code.
V8 is Google’s open source JavaScript engine. Chrome, Node.js, and many other applications use V8. This article explains V8’s bytecode format — which is actually easy to read once you understand some basic concepts.
"When V8 compiles JavaScript code, the parser generates an abstract syntax tree. A syntax tree is a tree representation of the syntactic structure of the JavaScript code. Ignition, the interpreter, generates bytecode from this syntax tree. TurboFan, the optimizing compiler, eventually takes the bytecode and generates optimized machine code from it."

NOTE : what modern browser have is they don't have one Compiler , they actually have two compiler : Baseline Compiler:(Interpreter:Ignition) and Optimizing Compiler(TurboFan)
How JavaScript is run in the browser??
- In programming, there are generally two ways of translating to machine language. You can use an interpreter or a compiler.
- With an interpreter, this translation happens pretty much line-by-line, on the fly.
- A compiler on the other hand doesn’t translate on the fly. It works ahead of time to create that translation and write it down.
- There are pros and cons to each of these ways of handling the translation.
Interpreter pros and cons
- Interpreters are quick to get up and running. You don’t have to go through that whole compilation step before you can start running your code. You just start translating that first line and running it.
- Because of this, an interpreter seems like a natural fit for something like JavaScript. It’s important for a web developer to be able to get going and run their code quickly.
And that’s why browsers used JavaScript interpreters in the beginning.
- But the con of using an interpreter comes when you’re running the same code more than once. For example, if you’re in a loop. Then you have to do the same translation over and over and over again.
Compiler pros and cons
The compiler has the opposite trade-offs.
- It takes a little bit more time to start up because it has to go through that compilation step at the beginning. But then code in loops runs faster, because it doesn’t need to repeat the translation for each pass through that loop.
- Another difference is that the compiler has more time to look at the code and make edits to it so that it will run faster. These edits are called optimizations.
- The interpreter is doing its work during runtime, so it can’t take much time during the translation phase to figure out these optimizations.
Just-in-time compilers: the best of both worlds
- As a way of getting rid of the interpreter’s inefficiency—where the interpreter has to keep retranslating the code every time they go through the loop—browsers started mixing compilers in.
- Different browsers do this in slightly different ways, but the basic idea is the same. They added a new part to the JavaScript engine, called a monitor (aka a profiler). That monitor watches the code as it runs, and makes a note of how many times it is run and what types are used.
- At first, the monitor just runs everything through the interpreter.
- If the same lines of code are run a few times, that segment of code is called warm. If it’s run a lot, then it’s called hot.
Baseline compiler
- When a function starts getting warm, the JIT will send it off to be compiled. Then it will store that compilation.
- Each line of the function is compiled to a “stub”. The stubs are indexed by line number and variable type (I’ll explain why that’s important later). If the monitor sees that execution is hitting the same code again with the same variable types, it will just pull out its compiled version.
- That helps speed things up. But like I said, there’s more a compiler can do. It can take some time to figure out the most efficient way to do things… to make optimizations.
- The baseline compiler will make some of these optimizations (I give an example of one below). It doesn’t want to take too much time, though, because it doesn’t want to hold up execution too long.
- However, if the code is really hot—if it’s being run a whole bunch of times—then it’s worth taking the extra time to make more optimizations.
Optimizing compiler
- When a part of the code is very hot, the monitor will send it off to the optimizing compiler. This will create another, even faster, version of the function that will also be stored.
- In order to make a faster version of the code, the optimizing compiler has to make some assumptions.
- For example, if it can assume that all objects created by a particular constructor have the same shape—that is, that they always have the same property names, and that those properties were added in the same order— then it can cut some corners based on that.
- The optimizing compiler uses the information the monitor has gathered by watching code execution to make these judgments. If something has been true for all previous passes through a loop, it assumes it will continue to be true.
- But of course with JavaScript, there are never any guarantees. You could have 99 objects that all have the same shape, but then the 100th might be missing a property.
- So the compiled code needs to check before it runs to see whether the assumptions are valid. If they are, then the compiled code runs. But if not, the JIT assumes that it made the wrong assumptions and trashes the optimized code.
- Then execution goes back to the interpreter or baseline compiled version. This process is called deoptimization (or bailing out).
- Usually optimizing compilers make code faster, but sometimes they can cause unexpected performance problems. If you have code that keeps getting optimized and then deoptimized, it ends up being slower than just executing the baseline compiled version.
- Most browsers have added limits to break out of these optimization/deoptimization cycles when they happen. If the JIT has made more than, say, 10 attempts at optimizing and keeps having to throw it out, it will just stop trying.
An example optimization: Type specialization
- There are a lot of different kinds of optimizations, but I want to take a look at one type so you can get a feel for how optimization happens. One of the biggest wins in optimizing compilers comes from something called type specialization.
- The dynamic type system that JavaScript uses requires a little bit of extra work at runtime. For example, consider this code:
function arraySum(arr) {
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
}
- The
+=step in the loop may seem simple. It may seem like you can compute this in one step, but because of dynamic typing, it takes more steps than you would expect. - Let’s assume that
arris an array of 100 integers. Once the code warms up, the baseline compiler will create a stub for each operation in the function. So there will be a stub forsum += arr[i], which will handle the+=operation as integer addition. - However,
sumandarr[i]aren’t guaranteed to be integers. Because types are dynamic in JavaScript, there’s a chance that in a later iteration of the loop,arr[i]will be a string. Integer addition and string concatenation are two very different operations, so they would compile to very different machine code. - The way the JIT handles this is by compiling multiple baseline stubs. If a piece of code is monomorphic (that is, always called with the same types) it will get one stub. If it is polymorphic (called with different types from one pass through the code to another), then it will get a stub for each combination of types that has come through that operation.
- This means that the JIT has to ask a lot of questions before it chooses a stub.
- Because each line of code has its own set of stubs in the baseline compiler, the JIT needs to keep checking the types each time the line of code is executed. So for each iteration through the loop, it will have to ask the same questions.
- The code would execute a lot faster if the JIT didn’t need to repeat those checks. And that’s one of the things the optimizing compiler does.
- In the optimizing compiler, the whole function is compiled together. The type checks are moved so that they happen before the loop.
Some JITs optimize this even further. For example, in Firefox there’s a special classification for arrays that only contain integers. If
arr is one of these arrays, then the JIT doesn’t need to check if arr[i] is an integer. This means that the JIT can do all of the type checks before it enters the loop.
Optimizations techniques:
JavaScript engine applies a couple of optimization technique, to speed of your execution and compile time:
- The code that didn't change between last execution and the current execution is not necessarily re-compiled, but already compiled code is taken, which is of course a way faster if you go through interpreter, then compiler process again.
- The browser gives couple of features , so called API which are built in you can tap into from your JS code.
For eg :
- The browser API's are part of browser written in C++, but again that depends on the browser you are using the browser gives your communcation bridge.(i.e js objects or function)which are not available in your JS code, which you can simply call in Js code.
CONCLUSION
Even with these improvements, though, the performance of JavaScript can be unpredictable. And to make things faster, the JIT has added some overhead during runtime, including:
- optimization and de-optimization
- memory used for the monitor’s bookkeeping and recovery information for when bailouts happen
- memory used to store baseline and optimized versions of a function.
"Store information, Collect information by running, recompile assuming that we get exact same type of inputs like different values but similar types and the resulting code is really fast as long as you don't change your code ."











Comments
Post a Comment