Even Faster Web Sites
Steve Souders
[email protected] http://stevesouders.com/docs/sxsw-20090314.ppt Disclaimer: This content does not necessarily reflect the opinions of my employer.
the importance of frontend performance 9%
17%
91%
83%
iGoogle, primed cache
iGoogle, empty cache
time spent on the frontend www.aol.com www.ebay.com www.facebook.com www.google.com/search search.live.com/results www.msn.com www.myspace.com en.wikipedia.org/wiki www.yahoo.com www.youtube.com
Empty Cache 97% 95% 95% 47% 67% 98% 98% 94% 97% 98%
Primed Cache 97% 81% 81% 0% 0% 94% 98% 91% 96% 97% April 2008
14 RULES
1. MAKE FEWER HTTP REQUESTS 2. USE A CDN 3. ADD AN EXPIRES HEADER 4. GZIP COMPONENTS 5. PUT STYLESHEETS AT THE TOP 6. PUT SCRIPTS AT THE BOTTOM 7. AVOID CSS EXPRESSIONS 8. MAKE JS AND CSS EXTERNAL 9. REDUCE DNS LOOKUPS 10.MINIFY JS 11.AVOID REDIRECTS 12.REMOVE DUPLICATE SCRIPTS 13.CONFIGURE ETAGS 14.MAKE AJAX CACHEABLE
25% discount code: "ssouders25"
Sept 2007
June 2009
Even Faster Websites Split the initial payload Load scripts without blocking Couple asynchronous scripts Don't scatter inline scripts Split the dominant domain Flush the document early Use iframes sparingly Simplify CSS Selectors
Ajax performance (Doug Crockford) Writing efficient JavaScript (Nicholas Zakas) Creating responsive web apps (Ben Galbraith, Dion Almaer) Comet (Dylan Schiemann) Beyond Gzipping (Tony Gentilcore) Optimize Images (Stoyan Stefanov, Nicole Sullivan)
why focus on JavaScript? Yahoo! Wikipedia eBay AOL MySpace YouTube Facebook
scripts block <script src="A.js"> blocks parallel downloads and rendering
http://stevesouders.com/cuzillion/?ex=10008
MSN.com: parallel scripts MSN Scripts and other resources downloaded in parallel! How? Secret sauce?! var p= g.getElementsByTagName("HEAD")[0]; var c=g.createElement("script"); c.type="text/javascript"; c.onreadystatechange=n; c.onerror=c.onload=k; c.src=e; p.appendChild(c)
asynchronous script loading XHR Eval XHR Injection Script in Iframe Script DOM Element Script Defer document.write Script Tag
XHR Eval var xhrObj = getXHRObject(); xhrObj.onreadystatechange = function() { if ( xhrObj.readyState != 4 ) return; eval(xhrObj.responseText); }; xhrObj.open('GET', 'A.js', true); xhrObj.send('');
script must have same domain as main page must refactor script http://stevesouders.com/cuzillion/?ex=10009
XHR Injection var xhrObj = getXHRObject(); xhrObj.onreadystatechange = function() { if ( xhrObj.readyState != 4 ) return; var se=document.createElement('script'); document.getElementsByTagName('head') [0].appendChild(se); se.text = xhrObj.responseText; }; xhrObj.open('GET', 'A.js', true); xhrObj.send('');
script must have same domain as main page http://stevesouders.com/cuzillion/?ex=10015
Script in Iframe <iframe src='A.html' width=0 height=0 frameborder=0 id=frame1>
iframe must have same domain as main page must refactor script: // access iframe from main page window.frames[0].createNewDiv(); // access main page from iframe parent.document.createElement('div'); http://stevesouders.com/cuzillion/?ex=10012
Script DOM Element var se = document.createElement('script'); se.src = 'http://anydomain.com/A.js'; document.getElementsByTagName('head')[0] .appendChild(se);
script and main page domains can differ no need to refactor JavaScript
http://stevesouders.com/cuzillion/?ex=10010
Script Defer <script defer src='A.js'>
only supported in IE (just landed in FF 3.1) script and main page domains can differ no need to refactor JavaScript
http://stevesouders.com/cuzillion/?ex=10013
document.write Script Tag document.write("<scr" + "ipt type='text/javascript' src='A.js'>" + "");
parallelization only works in IE parallel downloads for scripts, nothing else all document.writes must be in same script block
http://stevesouders.com/cuzillion/?ex=10014
browser busy indicators
browser busy indicators
good to show busy indicators when the user needs feedback bad when downloading in the background
ensure/avoid ordered execution Ensure scripts execute in order: necessary when scripts have dependencies IE: http://stevesouders.com/cuzillion/?ex=10017 FF: http://stevesouders.com/cuzillion/?ex=10018
Avoid scripts executing in order: faster – first script back is executed immediately http://stevesouders.com/cuzillion/?ex=10019
load scripts without blocking
*
Only other document.write scripts are downloaded in parallel (in the same script block).
and the winner is... XHR Eval XHR Injection Script in iframe Script DOM Element Script Defer same domains
different domains
Script DOM Element Script Defer
XHR Eval XHR Injection Script in iframe Script DOM Element (IE)
no order
Script DOM Element
preserve order
no order
preserve order
no busy
Script DOM Element (FF) Script Defer (IE) no busy
XHR Injection XHR Eval Script DOM Element (IE)
Script DOM Element (FF) Script Defer (IE) Managed XHR Eval Managed XHR Injection
Managed XHR Injection Managed XHR Eval show busy
Managed XHR Injection Managed XHR Eval Script DOM Element
show busy
Script DOM Element (FF) Script Defer (IE) Managed XHR Eval Managed XHR Injection
load scripts without blocking don't let scripts block other downloads you can still control execution order, busy indicators, and onload event What about inline scripts?
synchronous JS example: menu.js <script src="menu.js" type="text/javascript"> <script type="text/javascript"> var aExamples = [ ['couple-normal.php', 'Normal Script Src'], ['couple-xhr-eval.php', 'XHR Eval'], ... ['managed-xhr.php', 'Managed XHR'] ]; function init() { EFWS.Menu.createMenu('examplesbtn', aExamples); } init();
asynchronous JS example: menu.js script DOM element approach
<script type="text/javascript"> var domscript = document.createElement('script'); domscript.src = "menu.js"; document.getElementsByTagName('head')[0].appendChild(domscri pt); var aExamples = [ ['couple-normal.php', 'Normal Script Src'], ['couple-xhr-eval.php', 'XHR Eval'], ... ['managed-xhr.php', 'Managed XHR'] ]; function init() { EFWS.Menu.createMenu('examplesbtn', aExamples); } init();
before after
load scripts without blocking
!IE
*
Only other document.write scripts are downloaded in parallel (in the same script block).
asynchronous scripts wrap-up
what about
inlined code that depends on the script?
what about
inlined code that depends on the script?
baseline coupling results (not good)
need a way to load scripts asynchronously AND preserve order *
Scripts download in parallel regardless of the Defer attribute.
coupling techniques hardcoded callback window onload timer degrading script tags script onload
technique 1: hardcoded callback <script type="text/javascript"> var aExamples = [['couple-normal.php', 'Normal Script Src'], ...]; function init() { EFWS.Menu.createMenu('examplesbtn', aExamples); } var domscript = document.createElement('script'); domscript.src = "menu.js"; document.getElementsByTagName('head')[0].appendChild(domscri pt);
init() is called from within menu.js not very flexible doesn't work for 3rd party scripts
technique 2: window onload <iframe src="menu.php" width=0 height=0 frameborder=0>
<script type="text/javascript"> var aExamples = [['couple-normal.php', 'Normal Script Src'], ...]; function init() { EFWS.Menu.createMenu('examplesbtn', aExamples); }
if ( window.addEventListener ) { window.addEventListener("load", init, false); } else if ( window.attachEvent ) { window.attachEvent("onload", init); }
init() is called at window onload must use async technique that blocks onload: Script in Iframe does this across most browsers
init() called later than necessary
technique 3: timer <script type="text/javascript"> var domscript = document.createElement('script'); domscript.src = "menu.js"; document.getElementsByTagName('head')[0].appendChild(domscript); var aExamples = [['couple-normal.php', 'Normal Script Src'], ...]; function init() { EFWS.Menu.createMenu('examplesbtn', aExamples); }
function initTimer(interval) { if ( "undefined" === typeof(EFWS) ) { setTimeout(initTimer, interval); } else { init(); } } initTimer(300);
load if interval too low, delay if too high slight increased maintenance – EFWS
John Resig's degrading script tags <script src="menu-degrading.js" type="text/javascript">
var aExamples = [['couple-normal.php', 'Normal Script Src'], ...]; function init() { EFWS.Menu.createMenu('examplesbtn', aExamples); }
init();
at the end of menu-degrading.js: var scripts = document.getElementsByTagName("script"); var cntr = scripts.length; while ( cntr ) { var curScript = scripts[cntr-1]; if (curScript.src.indexOf("menu-degrading.js") != -1) { eval( curScript.innerHTML ); break; } cntr--; }
cleaner clearer safer – inlined code not called if script fails no browser supports it http://ejohn.org/blog/degrading-script-tags/
technique 4: degrading script tags <script type="text/javascript"> var aExamples = [['couple-normal.php', 'Normal Script Src'],...]; function init() { EFWS.Menu.createMenu('examplesbtn', aExamples); } var domscript = document.createElement('script');
domscript.src = "menu-degrading.js"; if ( -1 != navigator.userAgent.indexOf("Opera") ) { domscript.innerHTML = "init();"; } else { domscript.text = "init();"; }
document.getElementsByTagName('head')[0].appendChild(domscript);
elegant, flexible (cool!) not well known doesn't work for 3rd party scripts (unless...)
technique 5: script onload <script type="text/javascript"> var aExamples = [['couple-normal.php', 'Normal Script Src'], ...]; function init() { EFWS.Menu.createMenu('examplesbtn', aExamples); } var domscript = document.createElement('script'); domscript.src = "menu.js";
domscript.onloadDone = false; domscript.onload = function() { if ( ! domscript.onloadDone ) { init(); } domscript.onloadDone = true; }; domscript.onreadystatechange = function() { if ( "loaded" === domscript.readyState ) { if ( ! domscript.onloadDone ) { init(); } domscript.onloadDone = true; } } document.getElementsByTagName('head')[0].appendChild(domscript);
pretty nice, medium complexity
what about
multiple scripts that depend on each other,
and inlined code that depends on the scripts? two solutions: − Managed XHR − DOM Element and Doc Write
multiple script example: menutier.js <script src="menu.js" type="text/javascript"> <script src="menutier.js" type="text/javascript"> <script type="text/javascript"> var aRaceConditions = [['couple-normal.php', 'Normal...]; var aWorkarounds = [['hardcoded-callback.php', 'Hardcod...]; var aMultipleScripts = [['managed-xhr.php', 'Managed XH...]; var aLoadScripts = [['loadscript.php', 'loadScript'], ...]; var aSubmenus = [["Race Conditions", aRaceConditions], ["Workarounds", aWorkarounds], ["Multiple Scripts", aMultipleScripts], ["General Solution", aLoadScripts]]; function init() { EFWS.Menu.createTieredMenu('examplesbtn', aSubmenus); }
technique 1: managed XHR <script type="text/javascript"> var aRaceConditions = [['couple-normal.php', 'Normal...]; var aWorkarounds = [['hardcoded-callback.php', 'Hardcod...]; var aMultipleScripts = [['managed-xhr.php', 'Managed XH...]; var aLoadScripts = [['loadscript.php', 'loadScript'], ...]; var aSubmenus = [["Race Conditions", aRaceConditions], ...]; function init() { EFWS.Menu.createTieredMenu('examplesbtn', aSubmenus); }
EFWS.Script.loadScriptXhrInjection("menu.js", null, true); EFWS.Script.loadScriptXhrInjection("menutier.js", init, true); before after
XHR Injection asynchronous technique does not preserve order – we have to add that
EFWS.loadScriptXhrInjection // Load an external script. // Optionally call a callback and preserve order. loadScriptXhrInjection: function(url, onload, bOrder) { var iQ = EFWS.Script.queuedScripts.length; if ( bOrder ) { var qScript = { response: null, onload: onload, done: false }; EFWS.Script.queuedScripts[iQ] = qScript; } add to queue (if bOrder) var xhrObj = EFWS.Script.getXHRObject(); xhrObj.onreadystatechange = function() { if ( xhrObj.readyState == 4 ) { save response to queue if ( bOrder ) { EFWS.Script.queuedScripts[iQ].response = xhrObj.responseText; EFWS.Script.injectScripts(); } else { process queue (next slide) eval(xhrObj.responseText); if ( onload ) { onload(); } or... eval now, call callback } } }; xhrObj.open('GET', url, true); xhrObj.send(''); }
EFWS.injectScripts // Process queued scripts to see if any are ready to inject. injectScripts: function() { var len = EFWS.Script.queuedScripts.length; for ( var i = 0; i < len; i++ ) { var qScript = EFWS.Script.queuedScripts[i]; if ( ! qScript.done ) { if not yet injected if ( ! qScript.response ) { // STOP! need to wait for this response break; bail – need to wait to preserve order } else { eval(qScript.response); if ( qScript.onload ) { qScript.onload(); } ready for this script, qScript.done = true; eval and call callback } } } }
preserves external script order non-blocking couples with inlined code works in all browsers works with scripts across domains
technique 2: DOM Element and Doc Write
Firefox & Opera – use Script DOM Element IE – use document.write Script Tag Safari, Chrome – no benefit; rely on Safari 4 and Chrome 2
EFWS.loadScripts loadScripts: function(aUrls, onload) { // first pass: see if any of the scripts are on a different domain var nUrls = aUrls.length; var bDifferent = false; for ( var i = 0; i < nUrls; i++ ) { if ( EFWS.Script.differentDomain(aUrls[i]) ) { bDifferent = true; break; } } // pick the best loading function var loadFunc = EFWS.Script.loadScriptXhrInjection; if ( bDifferent ) { if ( -1 != navigator.userAgent.indexOf('Firefox') || -1 != navigator.userAgent.indexOf('Opera') ) { loadFunc = EFWS.Script.loadScriptDomElement; } else { loadFunc = EFWS.Script.loadScriptDocWrite; } }
}
// second pass: load the scripts for ( var i = 0; i < nUrls; i++ ) { loadFunc(aUrls[i], ( i+1 == nUrls ? onload : null ), true); }
multiple scripts with dependencies <script type="text/javascript"> var aRaceConditions = [['couple-normal.php', 'Normal...]; var aWorkarounds = [['hardcoded-callback.php', 'Hardcod...]; var aMultipleScripts = [['managed-xhr.php', 'Managed XH...]; var aLoadScripts = [['loadscript.php', 'loadScript'], ...]; var aSubmenus = [["Race Conditions", aRaceConditions], ...]; function init() { EFWS.Menu.createTieredMenu('examplesbtn', aSubmenus); }
EFWS.Script.loadScripts(["menu.js", "menutier.js"], init);
scripts on same domain: order preserved, no blocking
scripts on different domain: order preserved: all loads scripts in parallel: all except Saf3, Chr1 load script and image in parallel: FF, Saf4, Chr2
asynchronous scripts wrap-up
case study: Google Analytics recommended pattern:
1
<script type="text/javascript"> var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www."); document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E")); <script type="text/javascript"> var pageTracker = _gat._getTracker("UA-xxxxxx-x"); pageTracker._trackPageview();
document.write Script Tag approach blocks other resources 1
http://www.google.com/support/analytics/bin/answer.py?hl=en&answer=55488
case study: dojox.analytics.Urchin
1
_loadGA: function(){ var gaHost = ("https:" == document.location.protocol) ? "https://ssl." : "http://www."; dojo.create('script', { src: gaHost + "google-analytics.com/ga.js" }, dojo.doc.getElementsByTagName("head")[0]); setTimeout(dojo.hitch(this, "_checkGA"), this.loadInterval); }, _checkGA: function(){ setTimeout(dojo.hitch(this, !window["_gat"] ? "_checkGA" : "_gotGA"), this.loadInterval); }, _gotGA: function(){ this.tracker = _gat._getTracker(this.acct); ... }
Script DOM Element approach "timer" coupling technique (script onload better) 1
http://docs.dojocampus.org/dojox/analytics/Urchin
asynchronous loading & coupling async technique: Script DOM Element – easy, cross-browser – doesn't ensure script order
coupling technique: script onload
– fairly easy, cross-browser – ensures execution order for external script and inlined code
bad: stylesheet followed by inline script browsers download stylesheets in parallel with other resources that follow... ...unless the stylesheet is followed by an inline script http://stevesouders.com/cuzillion/?ex=10021
best to move inline scripts above stylesheets or below other resources use Link, not @import
don't scatter inline scripts MSN Wikipedia eBay MySpace
iframes: most expensive DOM element load 100 empty elements of each type tested in all major 1 browsers
1
IE 6, 7, 8; FF 2, 3.0, 3.1b2; Safari 3.2, 4; Opera 9.63, 10; Chrome 1.0, 2.0
iframes block onload parent's onload doesn't fire until iframe and all its components are downloaded workaround for Safari and Chrome: set iframe src in JavaScript <iframe id=iframe1 src=""> <script type="text/javascript"> document.getElementById('iframe1').src="url";
scripts block iframe IE
script
Firefox
script
Safari Chrome Opera
script
no surprise – scripts in the parent block the iframe from loading
stylesheets block iframe (IE, FF) IE
stylesheet
Firefox
stylesheet
Safari Chrome Opera
stylesheet
surprise – stylesheets in the parent block the iframe or its resources in IE & Firefox
stylesheets after iframe still block (FF) IE
Firefox
Safari Chrome Opera
stylesheet
stylesheet
stylesheet
surprise – even moving the stylesheet after the iframe still causes the iframe's resources to be blocked in Firefox
iframes: no free connections parent
iframe
iframe shares connection pool with parent (here – 2 connections per server in IE 7)
flush the document early html image image script html image image script
call PHP's flush()
gotchas: – – – – –
PHP output_buffering – ob_flush() Transfer-Encoding: chunked gzip – Apache's DeflateBufferSize before 2.2.8 proxies and anti-virus software browsers – Safari (1K), Chrome (2K)
other languages:
$| or FileHandle autoflush (Perl), flush (Python), ios.flush (Ruby)
flushing and domain blocking you might need to move flushed resources to a domain different from the HTML doc blocked by HTML document
html image image script html image image script
different domains
case study: Google search google image image script image 204
takeaways focus on the frontend run YSlow: http://developer.yahoo.com/yslow this year's focus: JavaScript speed matters
impact on revenue Google: +500 ms → -20% traffic1 Yahoo: +400 ms → -5-9% full-page traffic 1 Amazon: +100 ms → -1% sales
2
1 2
http://home.blarg.net/~glinden/StanfordDataMining.2006-11-29.ppt http://www.slideshare.net/stoyan/yslow-20-presentation
cost savings hardware – reduced load bandwidth – reduced response size
http://billwscott.com/share/presentations/2008/stanford/HPWP-RealWorld.pdf
if you want better user experience more revenue reduced operating expenses the strategy is clear
Even Faster Web Sites
Steve Souders
[email protected] http://stevesouders.com/docs/sxsw-20090314.ppt