Garbage Collection
One of the biggest problems in having a smooth experience resides in JavaScript garbage collector(GC) pauses. In JavaScript you create objects but you don't release them explicitly. That's job of the garbage collector. The problem arises when GC decides to clean up your objects: execution is paused, GC decides which objects are no longer needed and then releases them.Zig-zag memory usage pattern while playing a JavaScript game.
To keep your framerate consistent, you should keep garbage creation as low as possible. Often objects are created with the new keyword e.g. new Image() but there are other constructs that allocate memory implicitly:
var foo = {}; //Creates new anonymus object
var bar = []; //Creates new array object
function(){} //Creates new function
You should avoid creating objects in tight loops (e.g. rendering loop). Try to allocate objects once and reuse them later. In languages like C++ sometimes developers use object pools to avoid performance hits associated with memory allocation and fragmentation. The same idea could be used in JavaScript to avoid GC pauses. Here you can find out more about object pools.
To better demonstrate implicit garbage creation, consider following function:
function foo(){
//Some calculations
return {bar: "foo"};
}
Each time this function is called it creates a new anonymous object that needs to be cleared at some point. Another performance hit comes from using array shorthand [] to clear your array:
var r = new Array("foo", "bar"); //New array filled with some values, same as ["foo", "bar"]
r = [];//Clear the array
As you can see, the second line creates a new array and marks the previous one as garbage. It's better to set the array length to 0:
r.length = 0;
Functions can wake up the GC as well. Consider the following:
function foo(){
return function(x, y){
return x + y;
};
}
In above code we return a function refrence from our foo function but we also allocate memory for our anonymous function. The above code could be rewritten to avoid GC:
function bar(x, y){
return x + y;
}
function foo(){
return bar;
}
An important thing to be aware of is that global variables are not cleaned up by the garbage collector during the life of your page. That means objects like the above functions are only created once so whenever possible use them to your advantage. Also globals are cleaned up when users refresh the page, navigate to another page or close your page.
These are straightforward ways for avoiding performance hits that come from GC but you should also be aware of other JavaScript library functions that may create objects. By knowing what values are returned from your library functions you could make better decisions about designing your code. For example, if you know that a library function may allocate memory and you use that function in a performance-critical section of your code you may want to rewrite or use a similiar but more effecient function.
JavaScript Internals
JavaScript engines do some preparation on your code (including some optimizations) before execution. Knowing what they do behind the scenes will enable you to write generally better code. Here is an overview of how two popular JavaScript engines (Google's V8 and Mozilla's SpiderMonkey) work under the hood: V8:- JavaScript is parsed and native machine code is generated for faster execution. The initial code is not highly optimized.
- A runtime profiler monitors the code being run and detects "hot" functions (e.g. code that runs for long time).
- Code that's flagged as "hot" will be recompiled and optimized.
- V8 can deoptimize previousely optimized code if it discovers that some of the assumptions it made about the optimized code were too optimistic.
- Objects in V8 are represented with hidden classes to improve property access.
- JavaScript is parsed and bytecode is generated.
- A runtime profiler monitors the code being run and detects "hot" functions (e.g. code that runs for long time).
- Code that's flagged as "hot" will be recompiled and optimized by Just-In-Time(JIT) compiler.
Deleting Object Properties
Avoid using the delete keyword for removing object properties if you can. Consider the following code:
var obj = { foo: 123 };
delete obj.foo;
typeof obj.foo == 'undefined' //true
It will force V8 to change obj's hidden class and run it on a slower code path. Same is true about other JavaScript engines that optimize for "hot" objects. If possible, it's better to null the properties of object instead of removing them.
Monomorphic Variables
Whenever possible try to keep your variables monomorphic. For example don't put different objects with different hidden classes in your arrays. Same applies to properties and function parameters. Functions that are supplied with constant parameter types perform faster than the ones with different parameters.
//Fast
//JS engine knows you want an array of 3 elements of integer type
var arr = [1, 2, 3];
//Slow
var arr = [1, "", {}, undefined, true];
The below function can be called with different parameter types(ints, strings, objects, etc) but it will make it slow:
function add(a, b){
return a + b;
}
//Slow
add(1, 2);
add('a', 'b');
add(undefined, obj);
Array of Numbers
Using an array is usually faster than accessing object properties. This is particularly beneficial when the array contains numbers. For example it's better to write vectors using arrays than with objects with x, y, z properties.Arrays with Holes
Avoid "holes" in your arrays. It will make things slower than it should. Holes are created by deleting elements or adding elements out of range of the array. For example:
var arr = [1, 2, 3, 4, 5];//Full array
delete arr[0];//Creates hole
arr[7] = 1; //Creates hole
var hArr = [0, 1, 2, 3, /* hole */, 5];//Holey array
Pre-allocating Large Arrays
Current implementations of SpiderMonkey and V8 favor growing over pre-allocating large arrays (with more than 64K elements). Keep in mind that this is largely implementation-dependent as some implementations such as Nitro(Safari) or Carakan(Opera) favors pre-allocated arrays.Object Declaration
I can't give you single method for creating objects as it's very engine-dependent but you can see current performance test reults for yourself here and decide what's best for your application.Integer Arithmetic
Use integers where possible. Because most programs use integers, modern JavaScript engines are optimized for integer operations. In JavaScript all numbers are Number type thus you can't directly specify storage type (int, float, double, etc) like other strongly typed languages. If your application is math heavy, one unintended floating point artimetic can degrade your application performance and it can spread through your application. For example:
function halfVector(v){
v[0] /= 2;
v[1] /= 2;
v[2] /= 2;
}
var v = [3, 5, 9];
halfVector(v);
In a strong typed language like C++ where we used int type we would get [1, 2, 4] as result but in our case we implicitly switched to floating point math in our code.
JavaScript engines use integer math operations where possible and its because modern processors execute integer operations faster than floating point operations. Unlike other objects and floating point values, common integer values are stored in memory where they don't require allocation.
To tell JavaScript engine we want to store integer values in our array in above example we could use bitwise or operator:
function halfIntVector(v){
v[0] = (v[0] / 2) | 0;
v[1] = (v[1] / 2) | 0;
v[2] = (v[2] / 2) | 0;
}
Result of the bitwise or operator is an integer, this way JavaScript engine knows that it should not allocate memory.
This came just in time! Been playing with JS the past few days for the first time in years, so I'm trying to figure out the best way to handle about basic tasks and such.
Some no-brainers here (if you understand GC), but also some catches. All very helpful in the end. I love how you related it to individual engines and explained what they're doing with your code.