All Javascript Optimization Tips & Techniques
Parsing Objects in Javascript đź”—
if you are using object like this.
const data = {foo: 42, bar: 1337, ... };
use the JSON.parse()
instead
const data = JSON.parse('{"foo":42,"bar":1337, ... }');
It seems slower, but in the real world IT IS WAY WAY FASTER.
Why is it faster? because JSON.parse()
has one token (
Javascript object literal), but the string literal has too many tokens. want to know more ? watch this video from Google chrome developers YouTube channel
.
If you want the benchmark results, see it here .
Use requestAnimationFrame for visual changes đź”—
When visual changes are happening on screen you want to do your work at the right time for the browser, which is right at the start of the frame. The only way to guarantee that your JavaScript will run at the start of a frame is to use requestAnimationFrame
.
function updateScreen(time) {
// Make visual updates here.
}
requestAnimationFrame(updateScreen);
Frameworks or code samples may use setTimeout
or setInterval
to do visual changes like animations, but the problem with this is that the callback will run at some point in the frame, possibly right at the end, and that can often have the effect of causing us to miss a frame, resulting in jank.
In fact, jQuery used to use setTimeout
for its animate
behavior. It was changed to use requestAnimationFrame
in version 3. If you are using older version of jQuery, you can patch it to use requestAnimationFrame
, which is strongly recommended.
Reduce complexity or use Web Workers đź”—
JavaScript runs on the browser’s main thread, right alongside style calculations, layout, and, in many cases, paint. If your JavaScript runs for a long time, it will block these other tasks, potentially causing frames to be missed.
You should be tactical about when JavaScript runs, and for how long. For example, if you’re in an animation like scrolling, you should ideally be looking to keep your JavaScript to something in the region of 3-4ms. Any longer than that you risk taking up too much time. If you’re in an idle period, you can afford to be more relaxed about the time taken.
In many cases you can move pure computational work to Web Workers , if, for example, it doesn’t require DOM access. Data manipulation or traversal, like sorting or searching, are often good fits for this model, as are loading and model generation.
Here is a code snippet for workers.
var dataSortWorker = new Worker("sort-worker.js");
dataSortWorker.postMesssage(dataToSort);
// The main thread is now free to continue working on other things.
dataSortWorker.addEventListener('message', function(evt) {
var sortedData = evt.data;
// Update data on screen...
});
Unfortunately, Not all work can fit this model: Web Workers do not have DOM access. Where your work must be on the main thread, consider a batching approach, where you segment the larger task into micro-tasks, each taking no longer than a few milliseconds, and run inside of requestAnimationFrame
handlers across each frame.
var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
var taskFinishTime;
do {
// Assume the next task is pushed onto a stack.
var nextTask = taskList.pop();
// Process nextTask.
processTask(nextTask);
// Go again if there’s enough time to do the next task.
taskFinishTime = window.performance.now();
} while (taskFinishTime - taskStartTime < 3);
if (taskList.length > 0)
requestAnimationFrame(processTaskList);
}
There are UX and UI consequences to this approach, and you will need to ensure that the user knows that a task is being processed, either by using a progress or activity indicator . In any case this approach will keep your app’s main thread free, helping it to stay responsive to user interactions.
Know your JavaScript’s “frame tax” 🔗
When assessing a framework, library, or your own code, it’s important to assess how much it costs to run the JavaScript code on a frame-by-frame basis. This is especially important when doing performance-critical animation work like transitioning or scrolling.
The Performance panel of Chrome DevTools is the best way to measure your JavaScript’s cost. Typically you get low-level records like this:
The Main section provides a flame chart of JavaScript calls so you can analyze exactly which functions were called and how long each took.
Armed with this information you can assess the performance impact of the JavaScript on your application, and begin to find and fix any hotspots where functions are taking too long to execute. As mentioned earlier you should seek to either remove long-running JavaScript, or, if that’s not possible, move it to a Web Worker freeing up the main thread to continue on with other tasks.
Hidden Classes đź”—
JavaScript has limited compile-time type information: types can be changed at runtime, so it’s natural to expect that it is expensive to reason about JS types at compile time. This might lead you to question how JavaScript performance could ever get anywhere close to C++. However, V8 has hidden types created internally for objects at runtime; objects with the same hidden class can then use the same optimized generated code.
For example:
function Point(x, y) {
this.x = x;
this.y = y;
}
var p1 = new Point(11, 22);
var p2 = new Point(33, 44);
// At this point, p1 and p2 have a shared hidden class
p2.z = 55;
// warning! p1 and p2 now have different hidden classes!
Until the object instance p2 has additional member “.z” added, p1 and p2 internally have the same hidden class - so V8 can generate a single version of optimized assembly for JavaScript code that manipulates either p1 or p2. The more you can avoid causing the hidden classes to diverge, the better performance you’ll obtain.
Therefore:
- Initialize all object members in constructor functions (so the instances don’t change type later)
- Always initialize object members in the same order
Arrays in Javascript đź”—
In order to handle large and sparse arrays, there are two types of array storage internally:
- Fast Elements: linear storage for compact key sets
- Dictionary Elements: hash table storage otherwise
It’s best not to cause the array storage to flip from one type to another.
Therefore:
- Use contiguous keys starting at 0 for Arrays
- Don’t pre-allocate large Arrays (e.g. > 64K elements) to their maximum size, instead grow as you go
- Don’t delete elements in arrays, especially numeric arrays
- Don’t load uninitialized or deleted elements:
a = new Array();
for (var b = 0; b < 10; b++) {
a[0] |= b; // Oh no!
}
//vs.
a = new Array();
a[0] = 0;
for (var b = 0; b < 10; b++) {
a[0] |= b; // Much better! 2x faster.
}
Also, Arrays of doubles are faster - the array’s hidden class tracks element types, and arrays containing only doubles are unboxed (which causes a hidden class change).However, careless manipulation of Arrays can cause extra work due to boxing and unboxing - e.g.
var a = new Array();
a[0] = 77; // Allocates
a[1] = 88;
a[2] = 0.5; // Allocates, converts
a[3] = true; // Allocates, converts
is less efficient than:
var a = [77, 88, 0.5, true];
because in the first example the individual assignments are performed one after another, and the assignment of a[2] causes the Array to be converted to an Array of unboxed doubles, but then the assignment of a[3] causes it to be re-converted back to an Array that can contain any values (Numbers or objects). In the second case, the compiler knows the types of all of the elements in the literal, and the hidden class can be determined up front.
Therefore:
- Initialize using array literals for small fixed-sized arrays
- Preallocate small arrays (<64k) to correct size before using them
- Don’t store non-numeric values (objects) in numeric arrays
- Be careful not to cause re-conversion of small arrays if you do initialize without literals.
Avoid Unnecessary Steps đź”—
It is simple - if there is a straightforward way to do something, don’t work around !. Want to see a practical example ? here it is.
'incorrect'.split('').slice(2).join(''); // converts to an array
'incorrect'.slice(2); // remains a string
Want to learn more about unnecessary steps? read more here .
Break Out of Loops As Early As Possible đź”—
Look out for cases where it’s not necessary to complete every iteration in a loop. For example, if you’re searching for a particular value and find that value, subsequent iterations are unnecessary. You should break / terminate the execution of the loop by using a break
statement:
for (let i = 0; i < haystack.length; i++) {
if (haystack[i] === needle) break;
}
Or, if you need to perform actions on only certain elements in a loop, you can skip performing the actions on the other elements using the continue
statement. continue
terminates the execution of the statements in the current iteration and immediately moves on to the next one:
for (let i = 0; i < haystack.length; i++) {
if (!haystack[i] === needle) continue;
doSomething();
}
It’s also worth remembering that it’s possible to break out of nested loops using labels. These allow you to associate a break
or continue
statement with a specific loop:
loop1: for (let i = 0; i < haystacks.length; i++) {
loop2: for (let j = 0; j < haystacks[i].length; j++) {
if (haystacks[i][j] === needle) {
break loop1;
}
}
}
Minify for Production đź”—
Minify Javascript code before shipping it in production.
Avoid micro-optimizing your JavaScript đź”—
If you are creating a game or a computationally expensive application, micro-optimization is good. You should be insterested in things, like that requesting an element’s offsetTop
is faster than computing getBoundingClientRect()
.
But if you are creating a normal website which has no critical performance need, you should avoid micro-optimization. Optimizing your code to save fractions of milliseconds is worthless.
Conclusion đź”—
In brief,
- Use
JSON.parse()
instead of using Javascript object literal. - Use
requestAnimationFrame
instead of bothsetTimeout
andsetInterval
for visual updates. - Move long-running JavaScript off the main thread to Web Workers.
- Use micro-tasks to make DOM changes over several frames.
- Use Chrome DevTools’ Timeline and JavaScript Profiler to assess the impact of JavaScript.