Multiple sources for JS performance tips encourage developers to reduce "scope chain lookup". For example, IIFEs are touted as having a bonus benefit of "reducing scope chain lookup" when you access global variables. This sounds quite logical, perhaps even taken for granted, so I didn't question the wisdom. Like many others, I have been happily using IIFEs thinking that on top of avoiding global namespace pollution, there's gonna be a performance boost over any global code.
What we expect today:
(function($, window, undefined) {
// apparently, variable access here is faster than outside the IIFE
})(jQuery, window);
Simplifying / extending this to a generalized case, one would expect:
var x = 0;
(function(window) {
// accessing window.x here should be faster
})(window);
Based on my understanding of JS, there is no difference between x = 1;
and window.x = 1;
in the global scope. Therefore, it is logical to expect them to be equally performant, right? WRONG. I ran some tests and discovered that there's a significant difference in access times.
Ok, maybe if I place the window.x = 1;
inside an IIFE, it should run even faster (even if just slightly), right? WRONG again.
Ok, maybe it's Firefox; let's try Chrome instead (V8 is the benchmark for JS speed, yea?) It should beat Firefox for simple stuff like accessing a global variable directly, right? WRONG yet again.
So I set out to find out exactly which method of access is fastest, in each of the two browsers. So let's say we start with one line of code: var x = 0;
. After x
has been declared (and happily attached to window
), which of these methods of access would be fastest, and why?
Directly in global scope
x = x + 1;
Directly in global scope, but prefixed with window
window.x = window.x + 1;
Inside a function, unqualified
function accessUnqualified() {
x = x + 1;
}
Inside a function, with window
prefix
function accessWindowPrefix() {
window.x = window.x + 1;
}
Inside a function, cache window as variable, prefixed access (simulate local param of an IIFE).
function accessCacheWindow() {
var global = window;
global.x = global.x + 1;
}
Inside an IIFE (window as param), prefixed access.
(function(global){
global.x = global.x + 1;
})(window);
Inside an IIFE (window as param), unqualified access.
(function(global){
x = x + 1;
})(window);
Please assume browser context, i.e. window
is the global variable.
I wrote a quick time test to loop the increment operation a million times, and was surprised by the results. What I found:
Firefox Chrome
------- ------
1. Direct access 848ms 1757ms
2. Direct window.x 2352ms 2377ms
3. in function, x 338ms 3ms
4. in function, window.x 1752ms 835ms
5. simulate IIFE global.x 786ms 10ms
6. IIFE, global.x 791ms 11ms
7. IIFE, x 331ms 655ms
I repeated the test a few times, and the numbers appear to be indicative. But they are confusing to me, as they seem to suggest:
- prefixing with
window
is much slower (#2 vs #1, #4 vs #3). But WHY?
- accessing a global in a function (supposedly extra scope lookup) is faster (#3 vs #1). WHY??
- Why are the #5,#6,#7 results so different across the two browsers?
I understand there are some who think such tests are pointless for performance tuning, and that may well be true. But please, for the sake of knowledge, just humor me and help improve my understanding of these simple concepts like variable access and scope chain.
If you have read this far, thank you for your patience. Apologies for the long post, and for possibly lumping multiple questions into one - I think they are all somewhat related.
Edit: Sharing my benchmark code, as requested.
var x, startTime, endTime, time;
// Test #1: x
x = 0;
startTime = Date.now();
for (var i=0; i<1000000; i++) {
x = x + 1;
}
endTime = Date.now();
time = endTime - startTime;
console.log('access x directly - Completed in ' + time + 'ms');
// Test #2: window.x
x = 0;
startTime = Date.now();
for (var i=0; i<1000000; i++) {
window.x = window.x + 1;
}
endTime = Date.now();
time = endTime - startTime;
console.log('access window.x - Completed in ' + time + 'ms');
// Test #3: inside function, x
x =0;
startTime = Date.now();
accessUnqualified();
endTime = Date.now();
time = endTime - startTime;
console.log('accessUnqualified() - Completed in ' + time + 'ms');
// Test #4: inside function, window.x
x =0;
startTime = Date.now();
accessWindowPrefix();
endTime = Date.now();
time = endTime - startTime;
console.log('accessWindowPrefix()- Completed in ' + time + 'ms');
// Test #5: function cache window (simulte IIFE), global.x
x =0;
startTime = Date.now();
accessCacheWindow();
endTime = Date.now();
time = endTime - startTime;
console.log('accessCacheWindow() - Completed in ' + time + 'ms');
// Test #6: IIFE, window.x
x = 0;
startTime = Date.now();
(function(window){
for (var i=0; i<1000000; i++) {
window.x = window.x+1;
}
})(window);
endTime = Date.now();
time = endTime - startTime;
console.log('access IIFE window - Completed in ' + time + 'ms');
// Test #7: IIFE x
x = 0;
startTime = Date.now();
(function(global){
for (var i=0; i<1000000; i++) {
x = x+1;
}
})(window);
endTime = Date.now();
time = endTime - startTime;
console.log('access IIFE x - Completed in ' + time + 'ms');
function accessUnqualified() {
for (var i=0; i<1000000; i++) {
x = x+1;
}
}
function accessWindowPrefix() {
for (var i=0; i<1000000; i++) {
window.x = window.x+1;
}
}
function accessCacheWindow() {
var global = window;
for (var i=0; i<1000000; i++) {
global.x = global.x+1;
}
}
See Question&Answers more detail:
os