how edmunds got in the fast lane: 80% reduction in page load time in 3 simple steps
DESCRIPTION
(YouTube presentation is at the end of the slides) Back in the day, the onLoad event on our edmunds.com and insidelien.com pages used to take 9 seconds to fire! Yeah, we thought it was awful too. That's why in late 2008, we set out to do something about. In this deck, I will discuss the three main concepts that helped us take our pages from slow to really fast.TRANSCRIPT
HOW EDMUNDS GOT IN THE FAST LANE80% Reduction in Page Load Time in Simple Steps
by Ismail Elshareef
Sunday, January 16, 2011
The Results
onLoad: 8.4s ➙ 1.9s (≈ 80% reduction)
Page Views: 20%
Bounce Rate: 4%
Ad Impression Variance: 3%
Sunday, January 16, 2011
Online since 1995 Properties: 200M+ page views/month Revenue = Ads + Leads
Edmunds, Inc.
Sunday, January 16, 2011
Memory Lane
Sunday, January 16, 2011
Edmunds.com Legacy Site
Sunday, January 16, 2011
+100ms in response time ➡ -1% in sales
-30% in file size ➡ +30% in requests
“95% of Performance is Frontend” - Steve Souders
“Performance Matters!” - Organizations
Meanwhile ...
Sunday, January 16, 2011
The Real Picture!
Sunday, January 16, 2011
- 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
Site Condition
Sunday, January 16, 2011
We Started Tinkering ...
Sunday, January 16, 2011
- Solution Added Expires Header + Removed Etags
- Result: 34% reduction in bandwidth = 34TB annual savings = FREE video streaming for 2 years = Faster pages when cache is primed
Quick Wins: CACHING!
Sunday, January 16, 2011
... but Legacy was complicated.
Sunday, January 16, 2011
2008: A Vision Was Born
Sunday, January 16, 2011
PERFORMANCE (Faster Page Loads .. onLoad =< 1.5s)
RICHER CONTENT (Flash, video, slicker UXD, ...etc)
BETTER REVENUE (Positive impact on ad impressions)
Redesign Objectives
Sunday, January 16, 2011
FASTER PAGES
POSITIVE USER EXPERIENCE
HIGHER REVENUE
The Mindset
Sunday, January 16, 2011
HTTP Requests Page Performance
User
Experience
The Challenge
Sunday, January 16, 2011
1) Our Own Requests (files served from our own domains)
2) 3rd-party Requests (files served from other domains)
Types of HTTP Requests
Sunday, January 16, 2011
JavaScript iFrame
Exist in Two Forms
Cons: - Fixed width/height
Pros: - Easy to lazy-load - Sandboxed Code
Cons: - Access to DOM - document.write
Pros: - Richer Content
3rd-party Requests
Sunday, January 16, 2011
JavaScript iFrame
AdsAnalytics
A/B Testing
Video Widgets
Monitoring
Surveys
Roles on Our Sites
3rd-party Requests
Sunday, January 16, 2011
3rd-party RequestsPage Performance
User
Experience
HTTP Requests
Our Challenge
Sunday, January 16, 2011
3rd-party iFrame 3rd-party JavaScript
Agreeable Problematic
Sunday, January 16, 2011
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
Mission: Control 3rd
Sunday, January 16, 2011
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
Mission: Control 3rd
Sunday, January 16, 2011
You Can Not Control Everything
Our New Found Creed
Sunday, January 16, 2011
Edmunds vs. 3rd-party Requests
!"#$
%"#$
&'()*'+,$-./).+0+$
12'345206$-./).+0+$
!"#$
%"#$
&'()*'+,$-./).+0+$
12'345206$-./).+0+$
!"#$
%"#$
&'()*'+,$-./).+0+$
12'345206$-./).+0+$
Old SitesNew Sites
Sunday, January 16, 2011
Make What You Control FAST
Our New Found Creed
Sunday, January 16, 2011
JavaScript Loader
Sunday, January 16, 2011
How Does it Work?
Sunday, January 16, 2011
❶ Quick page is served to user
How Does it Work?
Sunday, January 16, 2011
❷ Page components register themselves with the page
12
3
4
5
6
7
❶ Quick page is served to user
How Does it Work?
Sunday, January 16, 2011
❷ Page components register themselves with the page
12
3
4
5
6
7
❶ Quick page is served to user
❸ Registered components rendered in parallel
How Does it Work?
Sunday, January 16, 2011
Component 6
Component 2Component 1
Component 3
Component 4
Component 5
Component 7
Component 8
I need flash.js and here’s my code. I need to be rendered right away
I need YUI’s Carousel module and here’s my code. I am below the fold so I can wait
I need YUI’s Carousel module. Here’s my code, render me right away!
Registration Process
Sunday, January 16, 2011
Component 6
Component 2Component 1
Component 3
Component 4
Component 5
Component 7
Component 8
I need flash.js and here’s my code. I need to be rendered right away
I need YUI’s Carousel module and here’s my code. I am below the fold so I can wait
I need YUI’s Carousel module. Here’s my code, render me right away!
① Declare Dependencies
Registration Process
Sunday, January 16, 2011
Component 6
Component 2Component 1
Component 3
Component 4
Component 5
Component 7
Component 8
I need flash.js and here’s my code. I need to be rendered right away
I need YUI’s Carousel module and here’s my code. I am below the fold so I can wait
I need YUI’s Carousel module. Here’s my code, render me right away!
① Declare Dependencies
② Submit Functionality
Registration Process
Sunday, January 16, 2011
Component 6
Component 2Component 1
Component 3
Component 4
Component 5
Component 7
Component 8
I need flash.js and here’s my code. I need to be rendered right away
I need YUI’s Carousel module and here’s my code. I am below the fold so I can wait
I need YUI’s Carousel module. Here’s my code, render me right away!
① Declare Dependencies
② Submit Functionality
③ Set Priority
Registration Process
Sunday, January 16, 2011
PAGESETUP.files.push('file1.js');PAGESETUP.files.push('file2.js');PAGESETUP.files.push('file7.js');
PAGESETUP.addControl(function() { // .... // Anything from rendering a component to making an AJAX call. All goes here. // ...}, 'high');
Declare Dependencies
Submit functionality
Set Priority
Registration Process
Sunday, January 16, 2011
function load() { // ..... // ..... 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(); } }}
Process Dependencies
Sunday, January 16, 2011
function load() { // ..... // ..... 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(); } }}
Get a reference to the function Process
Dependencies
Sunday, January 16, 2011
function load() { // ..... // ..... 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(); } }}
Process unique dependencies
Get a reference to the function Process
Dependencies
Sunday, January 16, 2011
function load() { // ..... // ..... 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(); } }}
Process unique dependencies
For all browsers but FireFox < 4, Download dependencies serially ...
Get a reference to the function Process
Dependencies
Sunday, January 16, 2011
function load() { // ..... // ..... 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(); } }}
Process unique dependencies
For all browsers but FireFox < 4, Download dependencies serially ...
Otherwise, download in parallel
Get a reference to the function Process
Dependencies
Sunday, January 16, 2011
function load() { // ..... // ..... 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(); } }}
Process unique dependencies
For all browsers but FireFox < 4, Download dependencies serially ...
Otherwise, download in parallel
When done downloading, render registered components!
Get a reference to the function Process
Dependencies
Sunday, January 16, 2011
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); },
Rendering Components
Sunday, January 16, 2011
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); },
Combine functions submitted by
components in priority order (high -> normal -> low)
Rendering Components
Sunday, January 16, 2011
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); },
Combine functions submitted by
components in priority order (high -> normal -> low)
Process one function at a time ...
Rendering Components
Sunday, January 16, 2011
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); },
Combine functions submitted by
components in priority order (high -> normal -> low)
Process one function at a time ...
every 25 ms!
Rendering Components
Sunday, January 16, 2011
Treat Everything Else as a Black Box
Our New Found Creed
Sunday, January 16, 2011
3rd-party iFrame 3rd-party JavaScript
Agreeable Problematic
Sunday, January 16, 2011
3rd-party iFrame 3rd-party JavaScript
Agreeable Problematic
Sunday, January 16, 2011
3rd-party Handling Logic
3rd-party?NO
Render before </html> in a hidden <div>
Placeholder Markup
Remove original call
Component on a page
Process through JS Loader
Relocate generated markup into placeholder
YES
iFrame
Register as "3rd-party" component
JavaScript
Sunday, January 16, 2011
// 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>
Rendering 3rd-party JavaScript:Part 1 - Register the Call
Sunday, January 16, 2011
// 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>
Rendering 3rd-party JavaScript:Part 1 - Register the Call
Placeholder Markup
Sunday, January 16, 2011
// 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>
Rendering 3rd-party JavaScript:Part 1 - Register the Call
Placeholder Markup
Register details about the JavaScript Request
Sunday, January 16, 2011
// 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>
Rendering 3rd-party JavaScript:Part 1 - Register the Call
Placeholder Markup
Register details about the JavaScript Request
Register: placeholder ID
Sunday, January 16, 2011
// 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>
Rendering 3rd-party JavaScript:Part 1 - Register the Call
Placeholder Markup
Register details about the JavaScript Request
Register: placeholder ID
Register: JavaScript Call
Sunday, January 16, 2011
<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>
Rendering 3rd-party JavaScript:Part 2 - Render at the bottom of the page
Sunday, January 16, 2011
<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>
Get the registered ID
Rendering 3rd-party JavaScript:Part 2 - Render at the bottom of the page
Sunday, January 16, 2011
<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>
Get the registered ID
Get the registered JavaScript Call
Rendering 3rd-party JavaScript:Part 2 - Render at the bottom of the page
Sunday, January 16, 2011
<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>
Get the registered ID
Get the registered JavaScript Call
Rendering 3rd-party JavaScript:Part 2 - Render at the bottom of the page
Wrap the call in a hidden DIV
Sunday, January 16, 2011
<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>
Get the registered ID
Get the registered JavaScript Call
Rendering 3rd-party JavaScript:Part 2 - Render at the bottom of the page
Wrap the call in a hidden DIV
Close the DIV
Sunday, January 16, 2011
Treat Everything Else as a Black Box
You Can Not Control Everything
Make What You Control FAST
In Summary
It’s OK, it really is :-)
No DOM Event dependencyRender components as soon as page is parsedRender components in priority
Process separatelyDelay inclusion as much as possibleIt’s about “mitigation” not “elimination”
Sunday, January 16, 2011
Inside Line Results
Sunday, January 16, 2011
PERFORMANCE (onLoad ≈1.5)
RICHER CONTENT (Large Flash content serving photos and videos)
BETTER REVENUE (Ad impression variance reduced by 3%)
Inside Line Redesign Objectives
Sunday, January 16, 2011
Another Vision Was Born
Sunday, January 16, 2011
Legacy Redesign
Sunday, January 16, 2011
Edmunds Legacy Edmunds Redesign
8.4 seconds 1.9 seconds
≈ 80% Reduction in Load Time!
Sunday, January 16, 2011
20%Total Page Views Per Session
The Results
Sunday, January 16, 2011
3%Ad Impression Variance
The Results
Sunday, January 16, 2011
4%Bounce Rate
The Results
Sunday, January 16, 2011
In The Works ...
Sunday, January 16, 2011
EVEN FASTER! Load in parallel and execute on-demand
COMPONENT METRICS (When it’s loaded, how long it took, ..etc)
MANY ENHANCEMENTS :)
JavaScript Loader 2.0
Sunday, January 16, 2011
http://www.github.com/edmunds
Soon ...
Sunday, January 16, 2011
What About The Future?
Sunday, January 16, 2011
Treat Everything Else as a Black Box
You Can Not Control Everything Make What You Control FAST
How Will It Apply To ...Sunday, January 16, 2011
Edmunds Mobile
Sunday, January 16, 2011
Edmunds Platform
Sunday, January 16, 2011
®
Widgets
Contentraw data
packaged data
Wired UI
Mobile UI
Backend Systems
Handheld
Syndication
Open Source
API
Products
Tools
Standalone
Plugins
Code SnippetsCars
Plug-n-Play
Managed
®
Edmunds Platform
Sunday, January 16, 2011
®
Edmunds Platform
3rd-party Consumer
3rd-party Provider
Sunday, January 16, 2011
Photo Credits:All Car Photos are from www.insideline.com http://www.cantronicsglobal.com/images/MissionVision.jpghttp://www.kewlwallpapers.com/images/wallpapers/John_C-680196.jpeg
Let’s Continue the Conversation@codeish|http://tech.edmunds.com
Sunday, January 16, 2011