Many resources providing JavaScript performance tips emphasize the importance of reducing "scope chain lookup" for better efficiency. One common suggestion is to use Immediately Invoked Function Expressions (IIFEs) which are said to have an added advantage of minimizing scope chain lookup when accessing global variables. This concept seemed logical and unquestionable, leading me to adopt IIFEs happily to prevent polluting the global namespace and potentially gain a performance boost over global code.
Our expectation today:
(function($, window, undefined) {
// it appears that variable access here is quicker than outside the IIFE
})(jQuery, window);
To simplify or extend this idea to a broader scenario, one would anticipate:
var x = 0;
(function(window) {
// it should be faster to access window.x here
})(window);
Based on my understanding of JavaScript, there should be no difference between x = 1;
and window.x = 1;
in the global scope. Therefore, one might assume they would perform similarly, right? WRONG. Through some testing, I discovered significant differences in access times.
Perhaps placing window.x = 1;
inside an IIFE could make it even faster (even if only slightly), correct? WRONG again.
Could it be due to Firefox? Let's try Chrome instead (known for its fast V8 engine in JS benchmarking). It should outperform Firefox when it comes to simple tasks like directly accessing a global variable, right? WRONG yet again.
I decided to investigate which method of access is truly the fastest in each browser. Let's start with one line of code: var x = 0;
. Once x
is declared and attached to window
, which of these methods would be the speediest, and why?
Directly in the global scope
x = x + 1;
Directly in global scope, but prefixed with
window
window.x = window.x + 1;
Inside a function without qualification
function accessUnqualified() { x = x + 1; }
Inside a function with
window
prefixfunction accessWindowPrefix() { window.x = window.x + 1; }
Inside a function, caching window as a variable for prefixed access (simulating local param of an IIFE)
function accessCacheWindow() { var global = window; global.x = global.x + 1; }
Inside an IIFE (window as parameter) with prefixed access
(function(global){ global.x = global.x + 1; })(window);
Inside an IIFE (window as parameter) without qualification
(function(global){ x = x + 1; })(window);
Please consider the browser context, where window
represents the global variable.
To test this, I conducted a quick comparison by looping the increment operation a million times. Here are the results I obtained:
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. Simulated IIFE global.x 786ms 10ms
6. IIFE, global.x 791ms 11ms
7. IIFE, x 331ms 655ms
After repeating the tests multiple times, the numbers seem consistent although somewhat perplexing. They indicate:
- Using
window
prefix is considerably slower (#2 vs #1, #4 vs #3). But WHY? - Accessing a global variable within a function (allegedly involving extra scope lookup) is actually faster (#3 vs #1). WHY??
- Why do the results for #5, #6, #7 differ so much between the two browsers?
While some may argue that such tests are irrelevant for performance optimization, I believe they can enhance our understanding of fundamental concepts like variable access and scope chain. So please indulge me and help clarify these queries for the sake of knowledge.
If you've reached this point, thank you for your patience. I apologize for the lengthy post and for potentially combining several questions into one - as I believe they are all interconnected.
Edit: Here is the benchmark code I used 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 (simulate 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;
}
}