The document discusses how Edmunds implemented a JavaScript loader to improve page load times by 80%. It reduced HTTP requests by loading components asynchronously and in parallel after an initial quick page load. Third-party requests were difficult to control, so Edmunds focused on optimizing its own content. The loader declared dependencies, prioritized components, and rendered them sequentially after dependencies were loaded to improve performance while supporting rich content.
6. Meanwhile ...
“95% of Performance
is Frontend” - Steve Souders
“Performance Matters!” - Organizations
+100ms in response time ➡ -1% in sales
-30% in file size ➡ +30% in requests
Sunday, January 16, 2011
8. Site Condition
- Too Many Requests (150+)
- Many Blocking Requests (20+)
- Perceived Slowness (onLoad in 8.4s)
- No Caching
- 1 Domains Serves All Requests
- External Factors (ads, videos, ..etc)
- Dependency on DOM Events
Sunday, January 16, 2011
13. Redesign Objectives
PERFORMANCE
(Faster Page Loads .. onLoad =< 1.5s)
RICHER CONTENT
(Flash, video, slicker UXD, ...etc)
BETTER REVENUE
(Positive impact on ad impressions)
Sunday, January 16, 2011
14. The Mindset
FASTER PAGES
POSITIVE USER EXPERIENCE
HIGHER REVENUE
Sunday, January 16, 2011
15. The Challenge
HTTP Requests Page Performance
User
Experience
Sunday, January 16, 2011
16. Types of HTTP Requests
1) Our Own Requests
(files served from our own domains)
2) 3rd-party Requests
(files served from other domains)
Sunday, January 16, 2011
17. 3rd-party Requests
Exist in Two Forms
JavaScript iFrame
Cons: Cons:
- Access to DOM - Fixed width/height
- document.write
Pros:
Pros: - Easy to lazy-load
- Richer Content - Sandboxed Code
Sunday, January 16, 2011
18. 3rd-party Requests
Roles on Our Sites
JavaScript iFrame
Analytics
A/B Testing
Ads
Video Widgets
Monitoring
Surveys
Sunday, January 16, 2011
19. Our Challenge
3rd-party Requests
HTTP Requests Page Performance
User
Experience
Sunday, January 16, 2011
21. Mission: Control 3rd
Attempt #1:
Override document.write()function
Example:
var buffer = [];
document.write = function(st) {
buffer.push(st);
}
// when a specific event occurs ...
var s = buffer.join(‘’);
document.getElementById(‘destination’).innerHTML = s;
Problem:
Didn’t work with daisy-chained document.write calls
Sunday, January 16, 2011
22. Mission: Control 3rd
Attempt #2:
Load in iFrame and Copy on Load (iFrame’n’Copy)
Example:
var jsAd = “http://ad.doubleclick.net/adj/....”;
iFrameObj.src = “http://www.edmunds.com/adSub.html?”+jsAd;
// Option 1: listen to iFrame onLoad event
// Option 2: iFrame page calls parent when done
Problem:
Buggy in IE 7 + Hard to Maintain
Sunday, January 16, 2011
23. Our New Found Creed
You Can Not Control Everything
Sunday, January 16, 2011
24. Edmunds vs. 3rd-party Requests
%"#$
%"#$
&'()*'+,$-./).+0+$
12'345206$-./).+0+$
!"#$
!"#$
New Sites Old Sites
&'()*'+,$-./).+0+$
12'345206$-./).+0+$
Sunday, January 16, 2011
25. Our New Found Creed
Make What You Control FAST
Sunday, January 16, 2011
28. How Does it Work?
❶ Quick page is
served to user
Sunday, January 16, 2011
29. How Does it Work?
2 1
❶ Quick page is
3
served to user
4
❷ Page components
register themselves
with the page
5
6
7
Sunday, January 16, 2011
30. How Does it Work?
2 1
❶ Quick page is
3
served to user
4
❷ Page components
register themselves
with the page
5
❸ Registered
components rendered 6
in parallel 7
Sunday, January 16, 2011
31. Component 1
Component 2 Component 3
Registration
Component 4
Component 5
Process
Component 6
I need YUI’s Carousel module. Here’s my
code, render me right away!
I need flash.js and here’s my code. I need to
be rendered right away
Component 7
I need YUI’s Carousel module and here’s
my code. I am below the fold so I can wait Component 8
Sunday, January 16, 2011
32. Component 1
Component 2 Component 3
Registration
Component 4
Component 5
Process
Declare Dependencies
Component 6
I need YUI’s Carousel module. Here’s my
code, render me right away!
I need flash.js and here’s my code. I need to
be rendered right away
Component 7
I need YUI’s Carousel module and here’s
my code. I am below the fold so I can wait Component 8
Sunday, January 16, 2011
33. Component 1
Component 2 Component 3
Registration
Component 4
Component 5
Process
Declare Dependencies
Component 6
I need YUI’s Carousel module. Here’s my
code, render me right away!
Submit Functionality
I need flash.js and here’s my code. I need to
be rendered right away
Component 7
I need YUI’s Carousel module and here’s
my code. I am below the fold so I can wait Component 8
Sunday, January 16, 2011
34. Component 1
Component 2 Component 3
Registration
Component 4
Component 5
Process
Declare Dependencies
Component 6
I need YUI’s Carousel module. Here’s my
code, render me right away!
Submit Functionality
I need flash.js and here’s my code. I need to
be rendered right away
Component 7
Set Priority
I need YUI’s Carousel module and here’s
my code. I am below the fold so I can wait Component 8
Sunday, January 16, 2011
35. Registration
Process
Declare Dependencies
PAGESETUP.files.push('file1.js');
PAGESETUP.files.push('file2.js');
PAGESETUP.files.push('file7.js');
Submit functionality
PAGESETUP.addControl(function() {
// ....
// Anything from rendering a component to making an AJAX call. All goes here.
// ...
}, 'high');
Set Priority
Sunday, January 16, 2011
36. Process
function load() {
// .....
// ..... Dependencies
var parent = arguments.callee;
if (unique_files.length) {
var file = unique_files.shift();
var js = document.createElement('script');
js.type = 'text/javascript';
js.src = file;
if (!FF || FF >= 2) {
js.onreadystatechange = function() {
if (this.readyState == 'complete' ||
this.readyState == 'loaded' ||
this.status == 304 ||
this.status == 404)
{
parent();
}
};
} else {
parent();
}
document.getElementsByTagName('head')[0].appendChild(js);
if (PAGESETUP.execControls) {
PAGESETUP.execControls();
}
}
}
Sunday, January 16, 2011
37. Get a reference to
the function Process
function load() {
// .....
// ..... Dependencies
var parent = arguments.callee;
if (unique_files.length) {
var file = unique_files.shift();
var js = document.createElement('script');
js.type = 'text/javascript';
js.src = file;
if (!FF || FF >= 2) {
js.onreadystatechange = function() {
if (this.readyState == 'complete' ||
this.readyState == 'loaded' ||
this.status == 304 ||
this.status == 404)
{
parent();
}
};
} else {
parent();
}
document.getElementsByTagName('head')[0].appendChild(js);
if (PAGESETUP.execControls) {
PAGESETUP.execControls();
}
}
}
Sunday, January 16, 2011
38. Get a reference to
the function Process
function load() {
// .....
// ..... Dependencies
var parent = arguments.callee;
if (unique_files.length) {
var file = unique_files.shift();
var js = document.createElement('script'); Process unique
js.type = 'text/javascript';
js.src = file; dependencies
if (!FF || FF >= 2) {
js.onreadystatechange = function() {
if (this.readyState == 'complete' ||
this.readyState == 'loaded' ||
this.status == 304 ||
this.status == 404)
{
parent();
}
};
} else {
parent();
}
document.getElementsByTagName('head')[0].appendChild(js);
if (PAGESETUP.execControls) {
PAGESETUP.execControls();
}
}
}
Sunday, January 16, 2011
39. Get a reference to
the function Process
function load() {
// .....
// ..... Dependencies
var parent = arguments.callee;
if (unique_files.length) {
var file = unique_files.shift();
var js = document.createElement('script'); Process unique
js.type = 'text/javascript';
js.src = file; dependencies
if (!FF || FF >= 2) {
js.onreadystatechange = function() {
if (this.readyState == 'complete' ||
For all browsers but
this.readyState == 'loaded' || FireFox < 4, Download
this.status == 304 ||
this.status == 404) dependencies serially ...
{
parent();
}
};
} else {
parent();
}
document.getElementsByTagName('head')[0].appendChild(js);
if (PAGESETUP.execControls) {
PAGESETUP.execControls();
}
}
}
Sunday, January 16, 2011
40. Get a reference to
the function Process
function load() {
// .....
// ..... Dependencies
var parent = arguments.callee;
if (unique_files.length) {
var file = unique_files.shift();
var js = document.createElement('script'); Process unique
js.type = 'text/javascript';
js.src = file; dependencies
if (!FF || FF >= 2) {
js.onreadystatechange = function() {
if (this.readyState == 'complete' ||
For all browsers but
this.readyState == 'loaded' || FireFox < 4, Download
this.status == 304 ||
this.status == 404) dependencies serially ...
{
parent();
}
}; Otherwise, download in
} else {
parallel
parent();
}
document.getElementsByTagName('head')[0].appendChild(js);
if (PAGESETUP.execControls) {
PAGESETUP.execControls();
}
}
}
Sunday, January 16, 2011
41. Get a reference to
the function Process
function load() {
// .....
// ..... Dependencies
var parent = arguments.callee;
if (unique_files.length) {
var file = unique_files.shift();
var js = document.createElement('script'); Process unique
js.type = 'text/javascript';
js.src = file; dependencies
if (!FF || FF >= 2) {
js.onreadystatechange = function() {
if (this.readyState == 'complete' ||
For all browsers but
this.readyState == 'loaded' || FireFox < 4, Download
this.status == 304 ||
this.status == 404) dependencies serially ...
{
parent();
}
}; Otherwise, download in
} else {
parallel
parent();
}
document.getElementsByTagName('head')[0].appendChild(js);
if (PAGESETUP.execControls) {
PAGESETUP.execControls();
}
}
When done downloading,
} render registered components!
Sunday, January 16, 2011
42. Rendering Components
execControls: function(start) {
// Get a timestamp here to indicate the start of the process
// Merge all the queues into one! high->normal->low
var merged = this.merged;
// Go through the merged array and execute the chunks!
setTimeout(function() {
// Get a chunk from the top of the array
var item = merged.shift();
if(item){
// Execute the chunk!
item.call();
}
if (merged.length > 0) {
// Wait 25 ms and then get the next chunk
setTimeout(arguments.callee, 25);
} else {
// Otherwise, get a timestamp to indicate the end of the process
}
}, 0);
},
Sunday, January 16, 2011
43. Rendering Components
execControls: function(start) {
// Get a timestamp here to indicate the start of the process
// Merge all the queues into one! high->normal->low
var merged = this.merged;
// Go through the merged array and execute the chunks!
setTimeout(function() {
// Get a chunk from the top of the array
Combine functions
var item = merged.shift();
submitted by if(item){
components in // Execute the chunk!
item.call();
priority order (high
}
-> normal -> low) if (merged.length > 0) {
// Wait 25 ms and then get the next chunk
setTimeout(arguments.callee, 25);
} else {
// Otherwise, get a timestamp to indicate the end of the process
}
}, 0);
},
Sunday, January 16, 2011
44. Rendering Components
execControls: function(start) {
// Get a timestamp here to indicate the start of the process
// Merge all the queues into one! high->normal->low
var merged = this.merged;
// Go through the merged array and execute the chunks!
setTimeout(function() {
// Get a chunk from the top of the array
Combine functions
var item = merged.shift();
submitted by if(item){
components in // Execute the chunk! Process one function at
item.call(); a time ...
priority order (high
}
-> normal -> low) if (merged.length > 0) {
// Wait 25 ms and then get the next chunk
setTimeout(arguments.callee, 25);
} else {
// Otherwise, get a timestamp to indicate the end of the process
}
}, 0);
},
Sunday, January 16, 2011
45. Rendering Components
execControls: function(start) {
// Get a timestamp here to indicate the start of the process
// Merge all the queues into one! high->normal->low
var merged = this.merged;
// Go through the merged array and execute the chunks!
setTimeout(function() {
// Get a chunk from the top of the array
Combine functions
var item = merged.shift();
submitted by if(item){
components in // Execute the chunk! Process one function at
item.call(); a time ...
priority order (high
}
-> normal -> low) if (merged.length > 0) {
// Wait 25 ms and then get the next chunk
setTimeout(arguments.callee, 25);
} else {
// Otherwise, get a timestamp to indicate the end of the process
} every 25 ms!
}, 0);
},
Sunday, January 16, 2011
46. Our New Found Creed
Treat Everything Else as a Black Box
Sunday, January 16, 2011
49. 3rd-party Handling Logic
Component Placeholder Markup
on a page
YES
NO 3rd-party?
Process through
JS Loader
JavaScript
iFrame
Render before </html> in a Register as "3rd-
Remove original call
hidden <div> party" component
Relocate generated
markup into
placeholder
Sunday, January 16, 2011
50. Rendering 3rd-party JavaScript:
Part 1 - Register the Call
// Add the placeholder
<div id="js-ad1"></div>
<script type="text/javascript">
(function() {
// Construct the ad URL
var ad = new EDMUNDS.AdUnit();
// Inform the PAGESETUP object of what’s going on
PAGESETUP.thirdpartyids.push('js-ad1');
PAGESETUP.thirdpartydetails['js-ad1'] = {};
PAGESETUP.thirdpartydetails['js-ad1']['type'] = 'ad';
PAGESETUP.thirdpartydetails['js-ad1']['src'] = ad.getAdUrl();
})();
</script>
Sunday, January 16, 2011
51. Rendering 3rd-party JavaScript:
Part 1 - Register the Call
Placeholder Markup
// Add the placeholder
<div id="js-ad1"></div>
<script type="text/javascript">
(function() {
// Construct the ad URL
var ad = new EDMUNDS.AdUnit();
// Inform the PAGESETUP object of what’s going on
PAGESETUP.thirdpartyids.push('js-ad1');
PAGESETUP.thirdpartydetails['js-ad1'] = {};
PAGESETUP.thirdpartydetails['js-ad1']['type'] = 'ad';
PAGESETUP.thirdpartydetails['js-ad1']['src'] = ad.getAdUrl();
})();
</script>
Sunday, January 16, 2011
52. Rendering 3rd-party JavaScript:
Part 1 - Register the Call
Placeholder Markup
// Add the placeholder
<div id="js-ad1"></div>
Register details about
<script type="text/javascript"> the JavaScript Request
(function() {
// Construct the ad URL
var ad = new EDMUNDS.AdUnit();
// Inform the PAGESETUP object of what’s going on
PAGESETUP.thirdpartyids.push('js-ad1');
PAGESETUP.thirdpartydetails['js-ad1'] = {};
PAGESETUP.thirdpartydetails['js-ad1']['type'] = 'ad';
PAGESETUP.thirdpartydetails['js-ad1']['src'] = ad.getAdUrl();
})();
</script>
Sunday, January 16, 2011
53. Rendering 3rd-party JavaScript:
Part 1 - Register the Call
Placeholder Markup
// Add the placeholder
<div id="js-ad1"></div>
Register details about
<script type="text/javascript"> the JavaScript Request
(function() {
// Construct the ad URL
var ad = new EDMUNDS.AdUnit();
// Inform the PAGESETUP object of what’s going on
Register: placeholder ID
PAGESETUP.thirdpartyids.push('js-ad1');
PAGESETUP.thirdpartydetails['js-ad1'] = {};
PAGESETUP.thirdpartydetails['js-ad1']['type'] = 'ad';
PAGESETUP.thirdpartydetails['js-ad1']['src'] = ad.getAdUrl();
})();
</script>
Sunday, January 16, 2011
54. Rendering 3rd-party JavaScript:
Part 1 - Register the Call
Placeholder Markup
// Add the placeholder
<div id="js-ad1"></div>
Register details about
<script type="text/javascript"> the JavaScript Request
(function() {
// Construct the ad URL
var ad = new EDMUNDS.AdUnit();
// Inform the PAGESETUP object of what’s going on
Register: placeholder ID
PAGESETUP.thirdpartyids.push('js-ad1');
PAGESETUP.thirdpartydetails['js-ad1'] = {};
PAGESETUP.thirdpartydetails['js-ad1']['type'] = 'ad';
PAGESETUP.thirdpartydetails['js-ad1']['src'] = ad.getAdUrl();
})();
</script>
Register: JavaScript Call
Sunday, January 16, 2011
55. Rendering 3rd-party JavaScript:
Part 2 - Render at the bottom of the page
<script type="text/javascript">
if (PAGESETUP.thirdpartyids.length > 0) {
(function() {
var id = PAGESETUP.thirdpartyids.shift();
var file = PAGESETUP.thirdpartydetails[id].src;
if (file) {
document.write('<div id="'+id+'-cache'+'" style="display:none;">
<div id="'+id+'-root'+'">');
document.write('<script type="text/javascript" src="'+file+'" ></script>');
}
})();
}
</script>
<script type="text/javascript">document.write('</div></div>');</script>
Sunday, January 16, 2011
56. Rendering 3rd-party JavaScript:
Part 2 - Render at the bottom of the page
Get the registered ID
<script type="text/javascript">
if (PAGESETUP.thirdpartyids.length > 0) {
(function() {
var id = PAGESETUP.thirdpartyids.shift();
var file = PAGESETUP.thirdpartydetails[id].src;
if (file) {
document.write('<div id="'+id+'-cache'+'" style="display:none;">
<div id="'+id+'-root'+'">');
document.write('<script type="text/javascript" src="'+file+'" ></script>');
}
})();
}
</script>
<script type="text/javascript">document.write('</div></div>');</script>
Sunday, January 16, 2011
57. Rendering 3rd-party JavaScript:
Part 2 - Render at the bottom of the page
Get the registered ID
<script type="text/javascript"> Get the registered
if (PAGESETUP.thirdpartyids.length > 0) {
(function() { JavaScript Call
var id = PAGESETUP.thirdpartyids.shift();
var file = PAGESETUP.thirdpartydetails[id].src;
if (file) {
document.write('<div id="'+id+'-cache'+'" style="display:none;">
<div id="'+id+'-root'+'">');
document.write('<script type="text/javascript" src="'+file+'" ></script>');
}
})();
}
</script>
<script type="text/javascript">document.write('</div></div>');</script>
Sunday, January 16, 2011
58. Rendering 3rd-party JavaScript:
Part 2 - Render at the bottom of the page
Get the registered ID
<script type="text/javascript"> Get the registered
if (PAGESETUP.thirdpartyids.length > 0) {
(function() { JavaScript Call
var id = PAGESETUP.thirdpartyids.shift();
var file = PAGESETUP.thirdpartydetails[id].src;
if (file) {
document.write('<div id="'+id+'-cache'+'" style="display:none;">
<div id="'+id+'-root'+'">');
document.write('<script type="text/javascript" src="'+file+'" ></script>');
}
})(); Wrap the call in a
}
</script> hidden DIV
<script type="text/javascript">document.write('</div></div>');</script>
Sunday, January 16, 2011
59. Rendering 3rd-party JavaScript:
Part 2 - Render at the bottom of the page
Get the registered ID
<script type="text/javascript"> Get the registered
if (PAGESETUP.thirdpartyids.length > 0) {
(function() { JavaScript Call
var id = PAGESETUP.thirdpartyids.shift();
var file = PAGESETUP.thirdpartydetails[id].src;
if (file) {
document.write('<div id="'+id+'-cache'+'" style="display:none;">
<div id="'+id+'-root'+'">');
document.write('<script type="text/javascript" src="'+file+'" ></script>');
}
})(); Wrap the call in a
}
</script> hidden DIV
<script type="text/javascript">document.write('</div></div>');</script>
Close the DIV
Sunday, January 16, 2011
60. In Summary
You Can Not Control Everything
It’s OK, it really is :-)
Make What You Control FAST
No DOM Event dependency
Render components as soon as page is parsed
Render components in priority
Treat Everything Else as a Black Box
Process separately
Delay inclusion as much as possible
It’s about “mitigation” not “elimination”
Sunday, January 16, 2011
70. JavaScript Loader 2.0
EVEN FASTER!
Load in parallel and execute on-demand
COMPONENT METRICS
(When it’s loaded, how long it took, ..etc)
MANY ENHANCEMENTS :)
Sunday, January 16, 2011
71. Soon ...
http://www.github.com/edmunds
Sunday, January 16, 2011
73. You Can Not Control Everything
Make What You Control FAST
Treat Everything Else as a Black Box
How Will It Apply To ...
Sunday, January 16, 2011
76. Edmunds Platform
Plug-n-Play
Widgets
Syndication
Managed
Products
raw data Content ®
Standalone
packaged data Tools
Open Source Plugins
Code Snippets
Cars
Mobile UI
Handheld
Wired UI
API
Backend Systems
Sunday, January 16, 2011
78. Let’s Continue the Conversation
@codeish http://tech.edmunds.com
Photo Credits:
All Car Photos are from www.insideline.com
http://www.cantronicsglobal.com/images/MissionVision.jpg
http://www.kewlwallpapers.com/images/wallpapers/John_C-680196.jpeg
Sunday, January 16, 2011