Write high-performance JavaScript
Translator's note: I translated foreign languages for the first time, and the words are difficult to understand. However, I tried to express the author's original intention as much as possible. without too much polishing, I welcome criticism and correction. In addition, this article is long and informative, which may be difficult to digest. You are welcome to leave a message to discuss the details. This article focuses on V8 performance optimization, and some content is not applicable to all JS engines. At last, please indicate the source for reprinting :)
======================================== ==================
Many JavaScript Engines, such as Google's V8 engine (used by Chrome and Node), are specifically designed for large-scale JavaScript applications that require fast execution. If you are a developer and are concerned about memory usage and page performance, you should understand how the JavaScript engine in your browser works. Whether it's V8, SpiderMonkey (Firefox)'s Carakan (Opera), Chakra (IE), or other engines, this will help youBetter optimize your application. This is not to say that we should optimize a browser or engine. Never do this.
However, you should ask yourself a few questions:
- Can I make the code more efficient in my code?
- Mainstream JavaScript Engines have been optimized
- What is engine optimization? Can the Garbage Collector (GC) recycle what I expect?
LoadA Fast website is like a fast sports car and requires customized parts. Image Source: dHybridcars.
There are some common traps when writing high-performance code. In this article, we will show some proven and better code writing methods.
So how does JavaScript work in V8?
If you do not have a deep understanding of the JS engine and there is no problem developing a large Web application, it is like driving people who just read the hood but have not seen the engine inside the hood. Since Chrome is my browser's first choice, let's talk about its JavaScript Engine. V8 is composed of the following core components:
- A basic CompilerIt parses the JavaScript code and generates the local machine code before the code is executed, instead of executing the bytecode or simply interpreting it. These codes are not highly optimized at the beginning.
- V8 builds an objectObject Model. Objects in JavaScript are represented as correlated arrays, but objects in V8 are considered as hidden classes, an internal type system designed to optimize queries.
- Runtime AnalyzerMonitors running systems and identifies "hot" functions (such as code that takes a long time to run ).
- Optimize the CompilerRecompile and optimize the code identified as "hot" by the runtime analyzer and perform "inline" optimization (for example, replacing the function call location with the caller's subject ).
- V8 supportOptimizationThis means that if the optimization compiler finds that the assumptions about code optimization are too optimistic, it will discard the optimized code.
- V8 hasGarbage CollectorTo understand how it works, just as important as optimizing JavaScript.
Garbage Collection
Garbage collection isMemory ManagementIt is actually the concept of a collector, trying to recycle the memory occupied by objects that are no longer in use. In a garbage collection language like JavaScript, objects still being referenced in an application are not cleared.
It is unnecessary to manually remove object references in most cases. By simply placing variables where they are needed (ideally, it is possible to be a local scope, that is, they are used in the function rather than the function outer layer), everything will work well.
The garbage collector tries to recycle memory. Image Source: Valtteri mäki.
In JavaScript, it is impossible to forcibly recycle garbage. You shouldn't do this because the garbage collection process is controlled by the runtime and it knows what is the best time to clean up.
"Eliminate the misunderstanding of reference"
Many discussions on JavaScript Memory recycling on the internet talk about the delete keyword, although it can be used to delete the attribute (key) in the object (map ), however, some developers think that it can be used to force "Remove references ". We recommend that you avoid using delete as much as possible. In the following exampleThe disadvantage of delete o. x is greater than the advantage because it changes the hidden class of o and makes it a "Slow object ".
var o = { x: 1 }; delete o.x; // true o.x; // undefined
You can easily find reference deletion in popular JS libraries-this is language-specific. Note that you should avoid modifying the structure of the "hot" object at runtime. The JavaScript engine can detect such "hot" objects and try to optimize them. If the structure of an object does not change significantly in the lifecycle, the engine will be more likely to optimize the object, and the delete operation will actually trigger this large structure change, which is not conducive to engine optimization.
There is also a misunderstanding about how null works. Setting an object reference to null does not make the object "null", but sets its reference to null. Using o. x = null is better than using delete, but it may not be necessary.
var o = { x: 1 }; o = null;o; // nullo.x // TypeError
If this reference is the final reference of the current object, the object will be reclaimed as garbage. If this reference is not the last reference of the current object, the object is accessible and will not be reclaimed.
In addition, global variables are not cleared by the garbage collector during the lifecycle of the page. No matter how long the page is opened, variables in the global object scope will always exist during JavaScript runtime.
var myGlobalNamespace = {};
The global object is cleared only when the page is refreshed, navigate to another page, close the tab, or exit the browser. The variables in the function scope will be cleared when they exceed the scope. That is, when the function is exited, no reference is provided, and such variables will be cleared.
Rule of thumb
To enable the Garbage Collector to collect as many objects as possible as soon as possible,Do not hold objects that are no longer in use. There are several things to remember:
- As mentioned above, using variables within the appropriate range is a better option to manually remove references. That is to say, a variable is only used in a function scope and should not be declared in a global scope. This means more clean and worry-free code.
- Make sure to unbind the event listeners that are no longer needed, especially the Event Listeners bound to the DOM object to be destroyed.
- If the data used is cached locally, make sure that the cache is cleared or the aging mechanism is used to avoid storing a large amount of unused data.
Function
Next, let's talk about functions. As we have already said, the working principle of garbage collection is to recycle memory blocks (objects) that are no longer accessed ). Here are some examples to better illustrate this point.
function foo() { var bar = new LargeObject(); bar.someCall();}
When foo is returned, the object pointed to by bar will be automatically recycled by the garbage collector because it has no reference.
Comparison:
function foo() { var bar = new LargeObject(); bar.someCall(); return bar;}// somewhere elsevar b = foo();
Now we have a reference pointing to the bar object, so that the life cycle of the bar object will continue from the call of foo to the variable B specified by the caller (or B-ultrasound out range ).
Closure (CLOSURES)
When you see a function and return an internal function, the internal function will gain access outside the scope, even after the external function is executed. This is a basic closure-a variable expression that can be set in a specific context. For example:
function sum (x) { function sumIt(y) { return x + y; }; return sumIt;}// Usagevar sumA = sum(4);var sumB = sumA(3);console.log(sumB); // Returns 7
In the sum call context, the generated function object (sumIt) cannot be recycled. It is referenced by the global variable (sumA) and can be called through sumA (n.
Let's take a look at another example. Can we access the variable largeStr?
var a = function () { var largeStr = new Array(1000000).join('x'); return function () { return largeStr; };}();
Yes, we can access largeStr through a (), so it is not recycled. What about the following?
var a = function () { var smallStr = 'x'; var largeStr = new Array(1000000).join('x'); return function (n) { return smallStr; };}();
We can no longer access largeStr, which is already a candidate for garbage collection. [Note: Because largeStr does not have any external reference]
Timer
One of the worst memory leaks is in loops or in setTimeout ()/setInterval (), but this is quite common. Consider the following example:
var myObj = { callMeMaybe: function () { var myRef = this; var val = setTimeout(function () { console.log('Time is running out!'); myRef.callMeMaybe(); }, 1000); }};
If we run myObj. callMeMaybe (); to start the timer, we can see that the console prints "Time is running out!" every second !". If you runmyObj =
Null. The timer is still active. For continuous execution, the closure passes myObj to setTimeout, so that myObj cannot be recycled. Instead, it references myObj because it captures myRef. This is the same as passing the closure to other functions to ensure reference.
It is also worth noting that references in setTimeout/setInterval calls (such as functions) will need to be executed and completed before they can be collected by garbage.
Beware of performance traps
Never optimize the code until you actually need it. Now we often see some benchmark tests, which show that N is more optimized than M in V8. However, we can test the module code or application and find that, the real effects of these optimizations are much smaller than you expected.
It is better not to do anything too much. Image Source: Tim Sheerman-Chase.
For example, we want to create such a module:
- A local data source must contain a numerical ID.
- Draw a table containing the data
- Add an event handler. When the user clicks any cell, switch the css class of the cell.
This problem has several different factors, although it is easy to solve. How can we store data, how can we efficiently draw tables and append them to the DOM, and how can we better process table events?
The first (naive) approach to these problems is to use an object to store data and put it into an array, use jQuery to traverse the data and draw a table and append it to the DOM, finally, we use events to bind the desired click behavior.
Note: This is not what you should do
var moduleA = function () { return { data: dataArrayObject, init: function () { this.addTable(); this.addEvents(); }, addTable: function () { for (var i = 0; i < rows; i++) { $tr = $('<tr></tr>'); for (var j = 0; j < this.data.length; j++) { $tr.append('<td>' + this.data[j]['id'] + '</td>'); } $tr.appendTo($tbody); } }, addEvents: function () { $('table td').on('click', function () { $(this).toggleClass('active'); }); } };}();
This code completes the task simply and effectively.
However, in this case, the data we traverse is simply the numeric attribute ID that should be stored in the array. Interestingly, using DocumentFragment and the local DOM method is a better choice than using jQuery (in this way) to generate a table. Of course, event proxy has higher performance than binding each td separately.
Although jQuery uses DocumentFragment internally, in our example, the Code calls append in a loop and these calls involve some other minor knowledge, so the optimization here is not very helpful. Hopefully this won't be a pain point, but be sure to perform a benchmark test to make sure your code is OK.
For our example, the above practices bring about (expected) performance improvement. The event proxy is an improvement for simple binding, and the optional DocumentFragment also plays a boosting role.
var moduleD = function () { return { data: dataArray, init: function () { this.addTable(); this.addEvents(); }, addTable: function () { var td, tr; var frag = document.createDocumentFragment(); var frag2 = document.createDocumentFragment(); for (var i = 0; i < rows; i++) { tr = document.createElement('tr'); for (var j = 0; j < this.data.length; j++) { td = document.createElement('td'); td.appendChild(document.createTextNode(this.data[j])); frag2.appendChild(td); } tr.appendChild(frag2); frag.appendChild(tr); } tbody.appendChild(frag); }, addEvents: function () { $('table').on('click', 'td', function () { $(this).toggleClass('active'); }); } };}();
Next, let's look at other ways to improve performance. You may have read where the prototype mode is better than the module mode, or you have heard that the JS template framework has better performance. This is true sometimes, but they are used to make the code more readable. By the way, there is also pre-compilation! Let's see how it works in practice?
moduleG = function () {};moduleG.prototype.data = dataArray;moduleG.prototype.init = function () { this.addTable(); this.addEvents();};moduleG.prototype.addTable = function () { var template = _.template($('#template').text()); var html = template({'data' : this.data}); $tbody.append(html);};moduleG.prototype.addEvents = function () { $('table').on('click', 'td', function () { $(this).toggleClass('active'); });};var modG = new moduleG();
It turns out that the performance improvement in this case is negligible. The selection of templates and prototypes does not really provide more things. That is to say, performance is not the reason why developers use them. The real reason is the readability, inheritance model, and maintainability of the Code.
More complex problems include efficiently Drawing Images and operating pixel data with or without arrays on the canvas.
Before using some methods in your own applications, be sure to learn more about the benchmarking of these solutions. Some people may remember the shoot-off and subsequent extension versions of the JS template. You need to figure out that the benchmark test does not exist in virtual applications that you cannot see, but should test the optimization in your actual code.
V8 optimization skills
The optimization points of each V8 engine are described in detail outside the scope of this article. Of course, there are also many tips worth mentioning here. Remember these skills and you will be able to reduce the Code with low performance.
- The specific mode can free V8 from the optimization dilemma, such as try-catch. To learn more about which functions can or cannot be optimized, you can use the-trace-opt file. js command in the V8 script tool d8.
- If you are concerned about speed, try to make your function have a single responsibility, that is, make sure that variables (including attributes, arrays, and function parameters) only use the same hidden class objects. For example, do not do this:
function add(x, y) { return x+y;} add(1, 2); add('a','b'); add(my_custom_object, undefined);
- Do not load uninitialized or deleted elements. If you do this, there will be no errors, but this will slow down the speed.
- Do not make the function body too large, which will make optimization more difficult.
For more information, see Daniel Clifford's sharing Breaking the JavaScript Speed Limit with V8 at Google I/O. Optimizing For V8-A Series is also worth reading.
Object VS array: Which one should I use?
- If you want to store a string of numbers or some objects of the same type, use an array.
- If you need a bunch of object attributes (different types), use an object and attribute. This is very efficient in memory and fast.
- Integer index elements, whether stored in an array or object, are much faster than the properties of the traversal object.
- The attributes of objects are complex: they can be created by setter and have different enumeration and writability. Arrays do not have such customization, but only exist in and out of the two States. At the engine level, this allows Optimization of more storage structures. Especially when there are numbers in the array, for example, when you need a container, you do not need to define a class with the x, y, and z attributes, but only use an array.
There is only one major difference between objects and arrays in JavaScript, that is, the magic length attribute of arrays. If you maintain this attribute by yourself, objects and arrays in V8 are as fast as they are.
Tips for using objects
- Create an object using a constructor. This ensures that all the objects it creates have the same hidden classes and helps avoid changing these classes. As an extra benefit, it is slightly faster than Object. create ()
- In your application, it is often harmful to use different types of objects and their complexity (within a reasonable range: Long-source chain, there is no limit to rendering objects with only a few attributes faster than large objects. For "hot" objects, try to keep the short prototype chain with fewer attributes.
Object cloning
Object cloning is a common problem for application developers. Although various benchmark tests can prove that V8 has handled this problem well, be careful. Copying large things is usually slow-Do not do this. The for. in loop in JS is particularly bad, because it has a devil-like specification, and no matter which engine it is in, it may never be faster than any object.
When you must copy an object in the key performance code path, use the array or a custom copy constructor function to explicitly copy each attribute. This may be the fastest way:
function clone(original) { this.foo = original.foo; this.bar = original.bar;}var copy = new clone(original);
Cache functions in module Mode
When the module mode is used, caching functions may improve the performance. Refer to the example below because it always creates a new copy of the member function, and the changes you see may be slow.
In addition, please note that using this method is obviously better than simply relying on the prototype mode (verified by jsPerf testing ).
Improved performance when using the module mode or prototype mode
This is a performance comparison test between the prototype mode and the module mode:
// Prototypal pattern Klass1 = function () {} Klass1.prototype.foo = function () { log('foo'); } Klass1.prototype.bar = function () { log('bar'); } // Module pattern Klass2 = function () { var foo = function () { log('foo'); }, bar = function () { log('bar'); }; return { foo: foo, bar: bar } } // Module pattern with cached functions var FooFunction = function () { log('foo'); }; var BarFunction = function () { log('bar'); }; Klass3 = function () { return { foo: FooFunction, bar: BarFunction } } // Iteration tests // Prototypal var i = 1000, objs = []; while (i--) { var o = new Klass1() objs.push(new Klass1()); o.bar; o.foo; } // Module pattern var i = 1000, objs = []; while (i--) { var o = Klass2() objs.push(Klass2()); o.bar; o.foo; } // Module pattern with cached functions var i = 1000, objs = []; while (i--) { var o = Klass3() objs.push(Klass3()); o.bar; o.foo; }// See the test for full details
Tips for using Arrays
Next, let's talk about array-related techniques. In general,Do not delete array elementsThis will make the array transition to a slow internal representation. When the index becomes sparse, V8 will convert the element into a slower dictionary mode.
Array literal
The array literal is very useful. It can imply the size and type of the VM array. It is usually used in a small array.
// Here V8 can see that you want a 4-element array containing numbers:var a = [1, 2, 3, 4];// Don't do this:a = []; // Here V8 knows nothing about the arrayfor(var i = 1; i <= 4; i++) { a.push(i);}
Single storage type VS multiple Storage types
It is never a good idea to store data of the mixed type (such as numbers, strings, undefined, true/false) in an array. For example, var arr = [1, "1", undefined, true, "true"]
Performance Test of type inference
As we can see, the integer array is the fastest.
Sparse array and full array
When using sparse arrays, note that accessing elements will be far slower than full arrays. V8 does not allocate a whole block of space to arrays that only use part of space. Instead, it is managed in the dictionary, which saves both space and access time.
Test sparse array and full array
Pre-allocated space VS Dynamic Allocation
Do not pre-allocate a large array (for example, an element larger than 64 K), its maximum size should be dynamic allocation. Before testing the performance of this article, remember that this applies only to some JavaScript Engines.
Empty literal and pre-allocated array are tested in different browsers
Nitro (Safari) is more advantageous for pre-allocated arrays. In other engines (V8, SpiderMonkey), pre-allocation is not efficient.
Pre-allocate array Test
// Empty arrayvar arr = [];for (var i = 0; i < 1000000; i++) { arr[i] = i;}// Pre-allocated arrayvar arr = new Array(1000000);for (var i = 0; i < 1000000; i++) { arr[i] = i;}
Optimize your application
In the world of Web applications, speed is everything. No user wants to use a table application that takes several seconds to calculate the total number of columns or takes several minutes to summarize information. This is why you need to squeeze every bit of performance in your code.
Image Source: Per Olof Forsberg.
It is very useful to understand and improve the performance of the application, but it is also difficult. We recommend the following steps to solve the performance pain points:
- Measurement: Find the slow location in your application (about 45%)
- Understanding: Find out what the actual problem is (about 45%)
- Fix it! (About 10%)
Some of the following recommended tools and technologies can help you.
BENCHMARKING)
There are many ways to run the JavaScript code snippet benchmark to test its performance-the general assumption is that the benchmark simply compares two timestamps. This mode is pointed out by the jsPerf team and used in SunSpider and Kraken's benchmarking suite:
var totalTime, start = new Date, iterations = 1000;while (iterations--) { // Code snippet goes here}// totalTime → the number of milliseconds taken // to execute the code snippet 1000 timestotalTime = new Date - start;
Here, the code to be tested is placed in a loop and runs a set number of times (for example, 6 times ). After that, the start date minus the end date will get the time it takes to execute the operation in the loop.
However, this benchmark is too simple, especially if you want to run the benchmark in multiple browsers and environments. The garbage collector itself has a certain impact on the results. These defects must be taken into account even if you use solutions such as window. performance.
No matter whether you only run the code of the benchmark part or not, write a test suite or code benchmark library. The JavaScript benchmark is actually more than you think. For more detailed guidelines, I strongly recommend that you read the Javascript benchmarks provided by Mathias Bynens and John-David Dalton.
Analysis (PROFILING)
The Chrome developer tool provides good support for JavaScript analysis. You can use this function to detect which functions take most of the time, so that you can optimize them. This is important. Even a small change in code will have an important impact on the overall performance.
Analysis panel of Chrome Developer Tools
The analysis process starts to obtain the code performance baseline, which is then presented in the form of a timeline. This tells us how long the code will take to run. The "Profiles" tab gives us a better insight into what is happening in the application. The JavaScript CPU analysis file shows how much CPU time is used for our code, the CSS selector analysis file shows how much time is spent on the processing selector, And the heap snapshot shows how much memory is being used for our objects.
Using these tools, we can separate, adjust, and re-analyze to determine whether our function or operation performance optimization is actually effective.
The "Profile" tab displays code performance information.
For a good introduction to The analysis, read Zack Grossbart's JavaScript Profiling With The Chrome Developer Tools.
Tip: Ideally, you can use--user-data-dir <empty_directory>
To start Chrome. In most cases, this method of optimization testing should be sufficient, but it also takes more time. This is the help of the V8 logo.
Avoid Memory leakage-3 Snapshot technology
In Google, Chrome developer tools are widely used by Gmail and other teams to help detect and eliminate memory leaks.
Memory statistics in Chrome Developer Tools
The memory counts private memory usage, JavaScript heap size, DOM node quantity, storage cleanup, event listening counters, and items to be recycled by our team. We recommend that you read Loreena Lee's "3 snapshots" technology. The key point of this technology is to record some behaviors in your application, force garbage collection, and check whether the number of DOM nodes has been restored to the expected baseline, then, analyze the three heap snapshots to determine whether memory leakage exists.
Memory Management for single-page applications
Memory Management of single-page applications (such as AngularJS, Backbone, and Ember) is very important, and they will almost never refresh the page. This means that the memory leakage may be quite obvious. Single page applications on mobile terminals are full of traps because devices have limited memory and applications such as Email clients or social networks are running for a long time.The larger the capacity, the heavier the responsibility.
There are many ways to solve this problem. In Backbone, make sure to use dispose () to process the old view and reference (currently available in Backbone (Edge ). This function is recently added. It removes the processing function added to the "event" Object of the view and the event listener of the model or collection that is passed to the third parameter (callback context) of the view. Dispose () is also called by remove () of the view to process the main cleaning work when the element is removed. When other libraries such as Ember detect that elements are removed, the listener is cleared to avoid Memory leakage.
Some wise suggestions from Derick Bailey:
Instead of understanding how events and references work, it is better to follow standard rules to manage memory in JavaScript. If you want to load data to a Backbone collection full of user objects, you need to clear the collection so that it no longer occupies memory, all references of the set and references of objects in the set are required. Once the reference is clear, the resource will be recycled. This is the standard JavaScript garbage collection rule.
In this article, Derick covers many common memory defects when using Backbone. js and how to solve these problems.
Felix Geisend örfer's debugging of memory leaks in Node is also worth reading, especially when it forms part of a broader SPA stack.
Reduce reflux (REFLOWS)
When the browser re-renders the elements in the document, it needs to re-calculate their location and geometric shape, which is called reflux. Reflux will block users' operations in the browser, so it is very helpful to improve the reflux time.
Reflux time chart
You should trigger backflow or re-painting in batches, but use these methods in a controlled manner. It is important not to process DOM as much as possible. You can use DocumentFragment, a lightweight Document Object. You can extract part of the document tree as a method, or create a new document "segment ". Instead of constantly adding DOM nodes, we recommend that you only perform DOM insertion once after using document fragments to avoid excessive backflow.
For example, we write a function to add 20 divs to an element. If you simply append a div to the element each time, this triggers 20 backflow.
function addDivs(element) { var div; for (var i = 0; i < 20; i ++) { div = document.createElement('div'); div.innerHTML = 'Heya!'; element.appendChild(div); }}
To solve this problem, you can use DocumentFragment instead. We can add a new div to it each time. After adding DocumentFragment to the DOM, only one backflow is triggered.
function addDivs(element) { var div; // Creates a new empty DocumentFragment. var fragment = document.createDocumentFragment(); for (var i = 0; i < 20; i ++) { div = document.createElement('a'); div.innerHTML = 'Heya!'; fragment.appendChild(div); } element.appendChild(fragment);}
See Make the Web Faster, JavaScript Memory Optimization, and Finding Memory Leaks.
JS Memory leakage Detector
To help detect JavaScript memory leaks, Google developers (Marja höltt äand Jochen Eisinger) developed a tool that is used in combination with Chrome developers, search for heap snapshots and find out what causes memory leakage.
A JavaScript Memory leakage detection tool
I have a complete article on how to use this tool. We recommend that you go to the Memory Leak Detector project page to see it yourself.
If you want to know why such a tool has not been integrated into our development tool, there are two reasons. It was initially used in the Closure library to help us capture some specific memory scenarios and is more suitable as an external tool.
V8 logo space for optimization debugging and garbage collection
Chrome supports passing some labels directly to V8 to obtain more detailed engine optimization output results. For example, we can track V8 optimization:
"/Applications/Google Chrome/Google Chrome" --js-flags="--trace-opt --trace-deopt"
Windows users can run chrome.exe-js-flags = "-trace-opt-trace-deopt"
The following V8 flag can be used when developing an application.
- Trace-opt -- record the name of the optimization function and display skipped code, because the optimizer does not know how to optimize it.
- Trace-deopt -- Record the code to be "optimized" at runtime.
- Trace-gc -- record each garbage collection.
The V8 processing script uses * (asterisk) to identify optimized functions ~ (Tilde) indicates an unoptimized function.
If you are interested