Transcript High Performance Web Sites 14 rules for faster
Even Faster Web Sites
Steve Souders
http://stevesouders.com/docs/google-20090305.ppt
Disclaimer: This content does not necessarily reflect the opinions of my employer.
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
Even Faster Web Sites
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) O'Reilly, June 2009
Why focus on JavaScript?
Scripts Block
asynchronous JS example: menu.js
after
baseline coupling results (not good)
normal Script Src XHR Eval XHR Injection Script in Iframe Script DOM Element Script Defer document.write
Script Tag Preserve Execution Order Load Script & Image in Parallel all FF, Op FF, Saf, Chr, Op all IE8, Saf4, Chr2 all all all IE, FF, Saf, Chr IE, (Saf4, Chr2) * Saf4, Chr2
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 script onload degrading script tags
technique 1: hardcoded callback
init() is called from within menu.js
not very flexible doesn't work for 3 rd party scripts
technique 2: window onload
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
load if interval too low, delay if too high slight increased maintenance – EFWS
technique 4: script onload
pretty nice, more complicated
John Resig's degrading script tags
var aExamples = [['couple-normal.php', 'Normal Script Src'], ...]; function init() { EFWS.Menu.createMenu('examplesbtn', aExamples); }
init();
at the end of menu-degrading.js: cleaner clearer
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--; }
safer – inlined code not called if script fails http://ejohn.org/blog/degrading-script-tags/
technique 5: degrading script tags
elegant, flexible (cool!) not well known doesn't work for 3 rd party scripts (unless...)
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
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
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; } var xhrObj = EFWS.Script.getXHRObject(); xhrObj.onreadystatechange = function() { if ( xhrObj.readyState == 4 ) { if ( bOrder ) { EFWS.Script.injectScripts();
add to queue (if bOrder) save response to queue
EFWS.Script.queuedScripts[iQ].response = xhrObj.responseText; } else { eval(xhrObj.responseText); if ( onload ) { onload(); } }
process queue (next slide) 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 ( ! qScript.response ) { } // STOP! need to wait for this response break; else { } eval(qScript.response); if ( qScript.onload ) { qScript.onload(); } qScript.done = true;
if not yet injected bail – need to wait to preserve order ready for this script, eval and call callback
} } preserves external script order couples with inlined code works with scripts across domains non-blocking works in all browsers
technique 2: DOM Element and Doc Write
Script DOM Element Script Defer document.write
Script Tag Preserve Execution Order Load Scripts in Parallel FF, Op FF, Op , IE, Saf, Chr Load Script & Image in Parallel FF , IE, Saf, Chr IE, Saf, Chr, FF, Op IE, Saf, Chr, FF, Op IE IE, Op IE
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); }
coupling wrap-up
Technique single script multiple scripts, no dependencies multiple scripts, dependencies, same domain multiple scripts, dependencies, same domain Script DOM Element Script DOM Element Managed XHR Script DOM Element (FF, Op), Doc Write (IE, Saf, Chr) Preserve Order Load Scripts in Parallel Load Script & Image in Parallel na na all na all all all all !Saf3, !Chr1
all all FF, Saf4, Chr2
case study: Google Analytics
recommended pattern: 1
document.write
Script Tag approach blocks other resources 1 http://www.google.com/support/analytics/bin/answer.py?hl=en&answer=55488
case study: Google Analytics
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
iframes: most expensive DOM element
load 100 empty elements of each type tested in all major browsers 1 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
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 stylesheet Firefox stylesheet Safari Chrome Opera 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 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)
flushing and domain blocking
html image image script html image image script you might need to move flushed resources to a domain different from the HTML doc google image image script image 204
Takeaways
focus on the frontend run YSlow: http://developer.yahoo.com/yslow this year's focus: JavaScript
Split the Initial Payload Load Scripts without Blocking Don't Scatter Inline Scripts
speed matters
Impact on Revenue
Google: Yahoo: Amazon: +500 ms -20% traffic 1 +400 ms -5-9% full-page traffic 2 +100 ms -1% sales 1 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/google-20090305.ppt