Translator's note: I am the first time I translated a foreign language, and my words are inevitably a bit obscure, but I tried my best to express the author's original intention and did not have much polish. Criticism and correction are welcome. In addition, this article is long and has a large amount of information, which may be difficult to digest. Please leave a message to discuss details. This article mainly focuses on the performance optimization of V8, and some of the content is not applicable to all JS engines. Finally, please indicate the source when reprinting: )
========================================================================
Many JavaScript engines, such as Google's V8 engine (used by Chrome and Node), are designed specifically for large JavaScript applications that require rapid 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) Carakan (Opera), Chakra (IE) or other engines, doing so can help you better optimize your application . This does not mean that you should optimize specifically for a certain browser or engine, and never do this.
However, you should ask yourself a few questions:
A fast- loading website is like a fast sports car that requires specially customized parts. Image source: dHybridcars.
There are some common pitfalls when writing high-performance code, and in this article we will show some proven and better ways to write code.
If you don't have a deep understanding of JS engines, there is no problem developing a large web application, just like a person who can drive has only seen the hood but not the engine inside the hood. Given that Chrome is my browser's first choice, let's talk about its JavaScript engine. V8 is composed of the following core parts:
Garbage collection is a form of memory management , which is actually a concept of a collector, trying to recycle memory occupied by objects that are no longer used. In a garbage collection language like JavaScript, objects that are still being referenced in the application will not be cleared.
Manually eliminating object references is not necessary in most cases. Everything will work just fine by simply putting the variables where they are needed (ideally, as locally scoped as possible, i.e., the function they are used instead of the outer layer of the function).
Garbage collector attempts to recycle memory. Image source: Valtteri Mäki.
In JavaScript, it is impossible to force garbage collection. 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.
There are many discussions on JavaScript memory recycling on the Internet about the keyword delete. Although it can be used to delete attributes (keys) in objects (map), some developers believe it can be used to force "dereferences". It is recommended to avoid using delete whenever possible. In the example below delete ox 的弊大于利,因为它改变了o的隐藏类,并使它成为一个"慢对象"。
var o = { x: 1 }; delete ox; // true ox; // undefinedYou will easily find reference removal in popular JS libraries - this is linguistically purposeful. It should be noted here that 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 object's structure does not change significantly during the life cycle, the engine will be easier for it to optimize the object, and the delete operation will actually trigger this larger structural change, which is not conducive to the engine's optimization.
There are also misunderstandings about how null works. Setting an object reference to null does not make the object "empty", it just sets its reference to empty. Using ox=null is better than using delete, but it may not be necessary.
var o = { x: 1 }; o = null;o; // nullo.x // TypeErrorIf this reference is the last reference to the current object, the object will be garbage collected. If this reference is not the last reference to the current object, the object is accessible and will not be garbage collected.
It should also be noted that global variables are not cleaned up by the garbage collector during the life cycle of the page. No matter how long the page is open, variables in the scope of the global object will always exist when the JavaScript runs.
var myGlobalNamespace = {};Global objects will only be cleaned when refreshing the page, navigating to another page, closing the tab, or exiting the browser. Variables in the function scope will be cleaned when they are out of scope, that is, when the function is exited, there are no references, and such variables will be cleaned.
In order for the garbage collector to collect as many objects as possible as possible, do not hold objects that are no longer used . Here are a few things to remember:
Next, let's talk about functions. As we have already said, garbage collection works by recycling blocks of memory (objects) that are no longer accessible. To illustrate this better, here are some examples.
function foo() { var bar = new LargeObject(); bar.someCall();}When foo returns, the object pointed to by bar will be automatically recycled by the garbage collector because it has no existing references.
Compare:
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 continues from the call to foo until the caller specifies another variable b (or b is out of scope).
When you see a function, return an internal function, which will get out of scope access, even after the external function is executed. This is a basic closure - an expression of variables 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 7The function object (sumIt) generated in the sum call context 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, where 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, it is already a garbage collection candidate. [Translator's note: Because largeStr no longer has external references]
One of the worst places to leak memory is in the loop, or in setTimeout()/setInterval(), but this is quite common. Think about the following examples:
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 myObj = null,定时器依旧处于激活状态。为了能够持续执行,闭包将myObj传递给setTimeout,这样myObj是无法被回收的。相反,它引用到myObj的因为它捕获了myRef。这跟我们为了保持引用将闭包传给其他的函数是一样的。
It is also worth remembering that references in setTimeout/setInterval calls (such as functions) will need to be executed and completed before they can be garbage collected.
Never optimize the code until you really need it. Now you can often see some benchmarks that show that N is more optimized in V8 than M, but if you test it in module code or application, you will find that these optimizations are really much smaller than you expect.
It’s better to do nothing than do it too much. Image source: Tim Sheerman-Chase.
For example, we want to create such a module:
There are several different factors in this problem, although it is also easy to solve. How do we store data, how do we efficiently draw tables and append them to the DOM, and how do we better handle table events?
The initial (naive) approach to facing these problems is to use objects to store data and put it into an array, use jQuery to traverse the data to draw a table and append it to the DOM, and finally use event binding to the click behavior we expect.
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 simply and effectively completes the task.
But in this case, the data we traverse is just a numeric property ID that should have been simply stored in the array. Interestingly, it is better to use DocumentFragment and local DOM methods directly than to generate tables using jQuery (in this way), and of course, event proxy has higher performance than binding each td alone.
Note that although jQuery uses DocumentFragment internally, in our example, the code calls append within a loop and these calls involve some other little knowledge, so the optimization effect here is not very good. Hopefully this won't be a pain point, but be sure to do a benchmark to make sure your code is OK.
For our example, the above practices bring (desired) performance improvements. Event proxying is an improvement to simple binding, and the optional DocumentFragment also helps.
var moduleD = function () { return { data: dataArray, init: function () { this.addTable(); this.addEvents(); }, addTable: function () { var td, tr; var fragment = document.createDocumentFragment(); var fragment2 = 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'); }); } };}();Let’s take a look at other ways to improve performance. You may have read that using prototype mode is better than module mode, or you have heard that using JS template frameworks perform better. Sometimes this is true, but they are used to make the code more readable. By the way, there is precompilation! Let's see how it performs 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 improvements brought in this situation are negligible. The choice of templates and prototypes doesn't really offer anything more. That is to say, performance is not the reason why developers use them, and the readability, inheritance model and maintainability brought to the code are the real reasons.
More complex problems include efficiently drawing pictures on canvas and manipulating pixel data with or without arrays of types.
Before using some methods for your own application, be sure to learn more about benchmarking of these solutions. Maybe someone still remembers the shoot-off and subsequent extensions of the JS template. You need to figure out that benchmarking does not exist in virtual applications you cannot see, but should test the optimizations brought by your actual code.
The optimization points of each V8 engine are introduced in detail outside the scope of this article. Of course, there are many tips worth mentioning here. Remember these tips and you can reduce the code that has poor performance.
function add(x, y) { return x+y;} add(1, 2); add('a','b'); add(my_custom_object, undefined);For more content, please see Daniel Clifford's sharing on Google I/O. Breaking the JavaScript Speed Limit with V8. Optimizing For V8 ― A Series is also very worth reading.
There is only one main difference between objects and arrays in JavaScript, that is, the magical length property of arrays. If you maintain this property yourself, then objects and arrays in V8 are as fast as those in arrays.
Object cloning is a common problem for application developers. While various benchmarks can prove that V8 handles this problem well, be careful. Copying big things is usually slower - don't do that. The for..in loop in JS is especially bad because it has a demonic specification and may never be faster than any object in any engine.
When you are sure to copy objects on the critical performance code path, use an array or a custom "copy constructor" function to explicitly copy each property. This is probably the fastest way:
function clone(original) { this.foo = original.foo; this.bar = original.bar;}var copy = new clone(original);Caching functions when using module mode may lead to performance improvements. See the example below, because it always creates a new copy of the member function, the changes you see may be slower.
Also note that using this method is obviously better, not just relying on the prototype mode (confirmed by jsPerf test).
Performance improvements when using module mode or prototype mode
This is a performance comparison test of prototype mode and module mode:
// Prototype 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 = 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 detailsNext, let’s talk about the techniques related to arrays. In general, do not delete array elements , which will make the array transition to a slower internal representation. When the index becomes sparse, V8 will turn the element into a slower dictionary pattern.
Array literals are very useful, and it can hint at the size and type of the VM array. It is usually used in arrays with small sizes.
// 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);}It is by no means a good idea to store data of mixed types (such as numbers, strings, undefined, true/false) in an array. For example var arr = [1, “1”, undefined, true, “true”]
Performance testing of type inference
As we have seen, arrays of integers are the fastest.
When you use sparse arrays, be careful to access elements will be much slower than full arrays. Because V8 will not allocate a whole piece of space to an array that only uses part of the space. Instead, it is managed in a dictionary, saving both space but taking time to access.
Testing of sparse arrays and full arrays
Do not pre-allocate large arrays (such as elements larger than 64K), their maximum size, but should be dynamically allocated. Before testing our performance in this article, remember that this applies only to some JavaScript engines.
Empty literals and pre-allocated arrays are tested in different browsers
Nitro (Safari) is more beneficial for pre-allocated arrays. In other engines (V8, SpiderMonkey), pre-allocation is not efficient.
Preallocated array testing
// 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;}In the world of web applications, speed is everything. No user wants to use a table application that takes seconds to calculate the total number of a column or to summarize information. This is an important reason why you want to squeeze every bit of performance in your code.
Image source: Per Olof Forsberg.
Understanding and improving the performance of your application is very useful, but it is also difficult. We recommend the following steps to solve performance pain points:
Some of the tools and techniques recommended below can help you.
There are many ways to run a benchmark for JavaScript code snippets to test its performance - the general assumption is that the benchmark simply compares two timestamps. This pattern is pointed out by the jsPerf team and is used in the benchmark suite of SunSpider and Kraken:
var totalTime, start = new Date, iterations = 1000;while (iterations--) { // Code snippet goes here}// totalTime → the number of millionseconds taken // to execute the code snippet 1000 timestotalTime = new Date - start;Here, the code to be tested is placed in a loop and run a set number of times (for example, 6 times). After this, the start date is subtracted from the end date, and the time it took to perform the operation in the loop is derived.
However, this benchmarking does things that are too simple, especially if you want to run benchmarks on multiple browsers and environments. The garbage collector itself has a certain impact on the results. Even if you use a solution like window.performance, these shortcomings must be taken into account.
Regardless of whether you only run the benchmark part of the code, write a test suite or code the benchmark library, JavaScript benchmarks are actually more than you think. For more detailed guide benchmarks, I highly recommend you read the Javascript benchmarks provided by Mathias Bynens and John-David Dalton.
Chrome developer tools have good support for JavaScript analytics. You can use this feature to detect which functions take up most of the time so you can optimize them. This is important, even small changes in the code can have a significant impact on the overall performance.
Chrome Developer Tools Analysis Panel
The analysis process begins to obtain the code performance baseline and then manifests it in the form of a timeline. This will tell us how long the code will take to run. The Profiles tab gives us a better perspective on what is going on in the application. JavaScript CPU analysis files show how much CPU time is used in our code, CSS selector analysis files show how much time is spent on processing selectors, and heap snapshots show how much memory is being used in our objects.
With these tools, we can separate, adjust and reanalyze to measure whether our functional or operational performance optimizations are actually effective.
The Profile tab displays code performance information.
A good introduction to analysis, read Zack Grossbart's JavaScript Profiling With The Chrome Developer Tools.
Tip: Ideally, if you want to make sure your analysis is not affected by any installed applications or extensions, you can use the --user-data-dir <empty_directory> flag to start Chrome. In most cases, this method optimization test should be sufficient, but it also takes more time for you. This is what the V8 logo can help.
Inside Google, Chrome developer tools are widely used by teams such as Gmail to help detect and troubleshoot memory leaks.
Memory statistics in Chrome Developer Tools
Memory counts the private memory usage, the size of the JavaScript heap, the number of DOM nodes, storage cleaning, event listening counters and garbage collectors that our team is concerned about. Recommended reading Loreena Lee's "3 Snapshot" technology. The key point of this technique is to log some behavior in your application, force garbage collection, check if the number of DOM nodes has been restored to the expected baseline, and then analyze snapshots of the three heaps to determine if there is a memory leak.
Memory management of single page applications (such as AngularJS, Backbone, Ember) is very important, they almost never refresh the page. This means that memory leaks may be quite obvious. Single-page applications on mobile terminals are full of pitfalls because the device has limited memory and runs applications such as email clients or social networks for a long time. The greater the ability, the heavier the responsibility.
There are many ways to solve this problem. In Backbone, make sure to use dispose() to handle old views and references (currently available in Backbone(Edge). This function is recently added, removing the handler added to the view's "event" object, and the event listener through the model or collection of the third parameter (callback context) passed to the view. dispose() will also be called by the view's remove(), handling the main cleanup work when the element is removed. Other libraries such as Ember clean up the listener to avoid memory leaks when they detect that elements are removed.
Some wise advice from Derick Bailey:
Instead of understanding how events and references work, follow standard rules to manage memory in JavaScript. If you want to load data into a Backbone collection full of user objects, you want to clear the collection so that it no longer takes up memory, then all references to the collection and references to the objects in the collection are required. Once the reference used is clear, the resource is recycled. This is the standard JavaScript garbage collection rules.
In the article, Derick covers many common memory flaws when using Backbone.js and how to solve these problems.
The tutorial on debugging memory leaks in Node by Felix Geisendörfer is also worth reading, especially when it forms part of a wider SPA stack.
When the browser re-renders elements in a document, they need to be recalculated and their positions and geometry, which we call reflow. Reflow blocks users' operations in the browser, so it is very helpful to understand that improving reflow time is improved.
Reflow time chart
You should trigger reflow or redraw in batches, but use these methods in moderation. It is also important to try not to deal with DOM. You can use DocumentFragment, a lightweight document object. You can use it as a way to extract part of the document tree, or create a new document "fragment". Instead of constantly adding DOM nodes, it is better to perform DOM insertion operations only once after using the document fragment to avoid excessive reflow.
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 will trigger 20 reflows.
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, we can use DocumentFragment instead, we can add a new div to it at a time. Adding DocumentFragment to the DOM after completion will only trigger a reflow once.
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.
To help discover JavaScript memory leaks, Google developers (Marja Hölttä and Jochen Eisinger) developed a tool that works in conjunction with Chrome developer tools to retrieve snapshots of the heap and detect what objects are causing the memory leak.
A JavaScript memory leak detection tool
There is a complete article about how to use this tool. It is recommended that you go to the Memory Leak Detector Project page for yourself.
If you want to know why such tools have not been integrated into our development tools, there are two reasons. It was originally designed to help us capture some specific memory scenarios in the Closure library, which is more suitable as an external tool.
Chrome supports passing some flags directly to V8 for more detailed engine optimization output results. For example, this can track the optimization of V8:
"/Applications/Google Chrome/Google Chrome" --js-flags="--trace-opt --trace-deopt"
Windows users can run chrome.exe js-flags="trace-opt trace-deopt"
When developing an application, the V8 logo below can be used.
V8's processing script uses * (asterisk) to identify optimized functions and uses ~ (wavy) to represent unoptimized functions.
If you are interested in learning more about the logo of the V8 and how the V8's interior works, it is highly recommended to read Vyacheslav Egorov's excellent post on V8 internals.
High Precision Time (HRT) is a submillisecond-level high-precision time interface that provides no impact on system time and user adjustments. It can be regarded as a more accurate measurement method than new Date and Date.now(). This helps us a lot in writing benchmarks.
High-precision time (HRT) provides current sub-millisecond time accuracy
Currently HRT is used in Chrome (stable version) in window.performance.webkitNow(), but the prefix is discarded in Chrome Canary, which makes it possible to call through window.performance.now(). Paul Irish posted more about HRT in HTML5Rocks.
Now that we know the current precise time, is there an API that can accurately measure page performance? Well, now there is a Navigation Timing API that provides an easy way to get accurate and detailed time measurement records when web pages are loaded and presented to users. You can use window.performance.timing in console to get time information:
Time information displayed in the console
We can obtain a lot of useful information from the above data, such as the network delay is responseEnd fetchStart, the page load time is loadEventEnd responseEnd, and the time for processing navigation and page loading is loadEventEnd navigationStart.
As you can see, the performance.memory property can also display the memory data usage of JavaScript, such as the total heap size.
For more details on the Navigation Timing API, read Sam Dutton's Measuring Page Load Speed With Navigation Timing.
About:tracing in Chrome provides a performance view of the browser, recording all threads, tab pages and processes of Chrome.
About:Tracing提供了浏览器的性能视图
The real purpose of this tool is to allow you to capture Chrome's running data so that you can properly adjust JavaScript execution, or optimize resource loading.
Lilli Thompson has an article for game developers about:tracing to analyze WebGL games, and is also suitable for JavaScript developers.
You can enter about:memory in the Chrome navigation bar, which is also very practical and can obtain the memory usage of each tab page, which is very helpful for locating memory leaks.
We see that there are many hidden traps in the JavaScript world , and there are no silver bullets that improve performance. Only by combining some optimization solutions into (real world) testing environments can you obtain the maximum performance benefits. Even so, understanding how the engine interprets and optimizes the code can help you tweak your application.
Measure, understand, repair. Repeat this process constantly.
Image source: Sally Hunter
Remember to pay attention to optimization, but for convenience, you can abandon some small optimizations. For example, some developers choose .forEach and Object.keys instead of for and for..in loops, although this will be slower but more convenient to use. We must ensure a clear mind and know what optimization is needed and what optimization is not needed.
Also note that while the JavaScript engine is getting faster and faster, the next real bottleneck is the DOM. Reduction of reflow and redraw is also important, so move the DOM if necessary. Also, we need to pay attention to the network. HTTP requests are precious, especially on mobile terminals. Therefore, HTTP cache should be used to reduce resource loading.
Remembering these points can ensure that you have obtained most of the information in this article. I hope it will be helpful to you!
Original text: http://coding.smashingmagazine.com/2012/11/05/writing-fast-memory-efficient-javascript/
Author: Addy Osmani