JS performance optimization from V8

JS performance optimization from V8

Featured on Hashnode

Note: This knowledge point belongs to the field of performance optimization.

Performance issues are becoming more and more hot topics in the front-end, because as the project gets bigger, performance issues are gradually manifested. In order to improve the user experience and reduce the loading time, the engineers did everything possible to optimize the details.

Before learning how to optimize performance, let's first understand how to test performance problems. After all, we will think about how to improve if there are problems first.

Test performance tools

Chrome already provides a large and comprehensive performance testing tool Audits

16772c479b194d48.webp

After clicking on Audits, you can see the following interface

16772c52e83d97c7.webp

In this interface, we can select the function we want to test and click Run audits , the tool will automatically run to help us test the problem and give a complete report

16772ca3d13a68ab.webp

The above picture is a report given after testing the performance of the Nuggets homepage. You can see that the report gives scores for performance, experience, and SEO, and each indicator has a detailed evaluation.

16772cae50f7eb81.webp

After the evaluation, the tool also provides some suggestions for us to improve the score of this indicator

16772cbdcdaccf15.webp

We only need to optimize the performance according to the suggestions one by one. In addition to the Audits tool, there is also a Performance tool at our disposal.

16772cf78a4fa18f.webp

In this figure, we can see in detail what the browser is processing in each time period, and which process consumes the most time, so that we can understand the performance bottleneck in more detail.

JS performance optimization

Whether JS is a compiled or interpreted language is not fixed. First of all, JS needs an engine to run, whether in a browser or in Node, which is a feature of interpreted languages. However, under the V8 engine, the TurboFan compiler was introduced. It will optimize under certain circumstances and compile the code into a more efficient Machine Code. Of course, this compiler is not necessary for JS, but only to improve the code. Execution performance, so in general JS is more inclined to interpreted languages.

Then the content of this section will mainly be explained for Chrome's V8 engine.

In this process, the JS code is first parsed into an abstract syntax tree (AST), and then converted into Bytecode or Machine Code by an interpreter or compiler

167736409eebe688.webp

From the above figure, we can see that JS will be parsed into AST first, and the parsing process is actually slightly slower. The more code, the longer the parsing process, which is one of the reasons why we need to compress the code. Another way to reduce parsing time is to pre-parse, which will act on unexecuted functions, which we will talk about later.

1677468f20b62240.webp

One thing to note here is that for functions, you should avoid declaring nested functions (classes are also functions) as much as possible, because this will cause repeated parsing of functions.

function test1() {
// will be parsed repeatedly
  function test2() {}
}

Then Ignition is responsible for converting AST into Bytecode, TurboFan is responsible for compiling optimized Machine Code, and Machine Code is better than Bytecode in execution efficiency

16773b904cfb732f.webp

JS is a dynamically typed language and has a whole bunch of rules. For simple addition code, several rules need to be considered internally, such as number addition, string addition, object and string addition, and so on. Such a situation will inevitably lead to a lot of internal judgment logic, which will reduce the operation efficiency.

function test(x) {
  return x + x
}

test(1)
test(2)
test(3)
test(4)

For the above code, if a function is called multiple times and the parameter is always passed in the number type, then V8 will think that this piece of code can be compiled into Machine Code, because you have fixed the type, there is no need to perform a lot of judgment logic.

But if the parameter type we pass in changes, then Machine Code will be DeOptimized to Bytecode, which will result in a performance loss. So if we want to compile more code into Machine Code and reduce the number of DeOptimized, we should ensure that the incoming type is as consistent as possible.

Then you may have a question, how much improvement is there before and after optimization, and then we will practice test to see how much improvement there is.

const { performance, PerformanceObserver } = require('perf_hooks')

function test(x) {
  return x + x
}
// PerformanceObserver only in node 10
// Node versions before this can directly use the API in performance
const obs = new PerformanceObserver((list, observer) => {
  console.log(list.getEntries())
  observer.disconnect()
})
obs.observe({ entryTypes: ['measure'], buffered: true })

performance.mark('start')

let number = 10000000
// Do not optimize code
%NeverOptimizeFunction(test)

while (number--) {
  test(1)
}

performance.mark('end')
performance.measure('test', 'start', 'end')

In the above code, we use the performance API, which is very useful for performance testing. Not only can it be used to measure the execution time of the code, but it can also be used to measure the time consumption in various network connections, etc., and this API can also be used in the browser.

16778338eb8b7130.webp

From the above figure, we can find that the execution time of the optimized code is only 9ms, but the execution time of the unoptimized code is twenty times that of the former, which is close to 200ms. In this case, I believe everyone has seen how strong the performance optimization of V8 is. As long as we write code according to certain rules, the bottom layer of the engine can help us optimize the code automatically.

In addition, the compiler also has an operation Lazy-Compile, when the function is not executed, it will pre-parse the function once, and it will not be parsed and compiled until the code is executed. For the above code, the test function needs to be pre-parsed once, and then parsed and compiled when it is called. But for this kind of function that is called immediately, the process of pre-parsing is actually redundant, so what is the way to prevent the code from being pre-parsed?

It's actually very simple, we just need to put parentheses around the function.

(function test(obj) {
  return x + x
})

But it is impossible for us to put parentheses on all functions for performance optimization, and not all functions need to do so. We can implement this function through optimize-js. This library will analyze the usage of some functions, and then add parentheses to the required functions. Of course, this library has not been maintained for a long time. If you need to use it, you still need to test the relevant content.

summary

Summarize what we have learned in this chapter

  • Performance reports for multiple metrics of the website can be obtained through the Audit tool

  • You can use the Performance tool to understand the performance bottlenecks of your website

  • Time can be measured specifically through the Performance API

  • In order to reduce compilation time, we can reduce the size of the code file or reduce the way of writing nested functions

  • In order for V8 to optimize the code, we should ensure that the types of the incoming parameters are consistent as much as possible. This also brings us a thought, is this also one of the benefits of using TypeScript?