Вы находитесь на странице: 1из 69

Even Faster Web Sites

Steve Souders
souders@google.com
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% 91%

17% 83%

iGoogle, primed cache

iGoogle, empty cache


time spent on the frontend
Empty Cache Primed Cache
www.aol.com 97% 97%
www.ebay.com 95% 81%
www.facebook.com 95% 81%
www.google.com/search 47% 0%
search.live.com/results 67% 0%
www.msn.com 98% 94%
www.myspace.com 98% 98%
en.wikipedia.org/wiki 94% 91%
www.yahoo.com 97% 96%
www.youtube.com 98% 97%
April 2008
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
14 RULES 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 Ajax performance (Doug


Load scripts without blocking Crockford)
Couple asynchronous scripts Writing efficient JavaScript
(Nicholas Zakas)
Don't scatter inline scripts
Creating responsive web apps
Split the dominant domain (Ben Galbraith, Dion Almaer)
Flush the document early Comet (Dylan Schiemann)
Use iframes sparingly Beyond Gzipping (Tony
Simplify CSS Selectors 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>

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'></script>

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'>" +
"</scr" + "ipt>");

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 no order preserve


Element order
Script Defer
XHR Eval Script DOM Element
no order XHR Injection (FF)
Script in iframe Script Defer (IE)
preserve
order Script DOM Element Managed XHR Eval
Script DOM (IE) Managed XHR
Element Injection
no busy show busy
Script DOM
Element (FF)
Script Defer (IE) Managed XHR Script DOM
Injection Element (FF)
Managed XHR Eval Script Defer (IE)
no busy
show busy Managed XHR Eval
Managed XHR
Injection
XHR Injection Managed XHR
XHR Eval Injection
Script DOM Element Managed XHR Eval
(IE) Script DOM Element
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>
<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();
</script>
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();
</script>
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);
</script>

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>
</iframe>
<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);
}
</script>

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);
</script>

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:
</script> 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;
cleaner }
cntr--;
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);
</script>

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);
</script>

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>
<script src="menutier.js" type="text/javascript"></script>
<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);
}
</script>
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);
</script>

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(); add to queue (if bOrder)
xhrObj.onreadystatechange = function() {
if ( xhrObj.readyState == 4 ) {
if ( bOrder ) { save response to queue
EFWS.Script.queuedScripts[iQ].response = xhrObj.responseText;
EFWS.Script.injectScripts();
}
else {
eval(xhrObj.responseText); process queue (next slide)
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();
}
qScript.done = true; ready for this script,
} 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);


</script>

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>
<script type="text/javascript">
var pageTracker = _gat._getTracker("UA-xxxxxx-x");
pageTracker._trackPageview();
</script>

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
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
<iframe id=iframe1 src=""></iframe>
<script type="text/javascript">
document.getElementById('iframe1').src="url";
</script>
scripts block iframe
IE script

Firefox script

Safari script
Chrome
Opera

no surprise – scripts in the parent block


the iframe from loading
stylesheets block iframe (IE, FF)
IE stylesheet

Firefox stylesheet

Safari stylesheet
Chrome
Opera

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 stylesheet
Opera

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
html
image
blocked by HTML document
image
script

html
image
image different domains
script

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 2

Amazon: +100 ms → -1% sales


1

1
http://home.blarg.net/~glinden/StanfordDataMining.2006-11-29.ppt
2
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
souders@google.com
http://stevesouders.com/docs/sxsw-20090314.ppt

Вам также может понравиться