前言
kyrieliuの《高性能JavaScript》讀書筆記。
script標簽是一個很“霸道”的狠角色,它的每次出現(xiàn)都讓頁面等待腳本的解析和執(zhí)行。也就是說,不管當前的javascript代碼是內(nèi)嵌還是包含在外鏈文件中膛壹,頁面的下載和渲染都必須停下來等待腳本執(zhí)行完成。
其實唉堪,script標簽的“霸道”是必須的模聋,因為頁面的生存周期中,腳本的執(zhí)行可能會修改頁面的內(nèi)容唠亚。
總之链方,在解析和執(zhí)行js的這個過程中,頁面渲染和用戶交互完全被阻塞了灶搜。
腳本位置
說起腳本的位置祟蚀,腦海里不禁想起來那兩句真言:
- css放在
<head>
里 - js放在
</body>
前
至于why呢,舉個栗砸:
<html>
<head>
<title>一個栗子</title>
<script type='text/javascript' src='js_file_1.js'></script>
<script type='text/javascript' src='js_file_2.js'></script>
<script type='text/javascript' src='js_file_3.js'></script>
<link rel='stylesheet' type='text/css' href='css_file.css' />
</head>
<body>
<p>hello from kyrieliu.</p>
</body>
</html>
這一段看似正常的代碼實際上有著肥腸嚴重的性能問題:在head中加載了三個js文件割卖。
前面我有說到前酿,js文件會阻塞頁面渲染,知道它們?nèi)肯螺d并執(zhí)行完畢后鹏溯,頁面渲染才能繼續(xù)(無情無恥無理取鬧)罢维。
縱觀一份html文檔,用戶真正看得見的內(nèi)容基本上都寫在body標簽里丙挽,也就是說肺孵,瀏覽器在解析到body標簽之前,不會渲染頁面的任何部分∪∈裕現(xiàn)在又寫了三個腳本到head標簽里面悬槽,好嘞,這下子渲染的延遲更明顯了瞬浓,用戶在打開這樣的一個頁面時,看到了what蓬坡?白屏猿棉!用戶不能瀏覽頁面的內(nèi)容,更無法與頁面進行交互屑咳。
場景還原
第一個js文件開始下載萨赁,與此同時阻塞了頁面其他文件的下載。等呀等兆龙,ok杖爽,第一個js文件終于下載完了敲董,第二個js按捺不住自己喜悅的心情,正要開始下載慰安,突然腋寨,第一個js文件說:“且慢,老子還沒有完事呢”化焕,第二個js文件嚇了一跳萄窜,站在原地不動。第一個js文件開始執(zhí)行撒桨,等到執(zhí)行完畢查刻,第二個js文件才得以開始下載。
總之凤类,每個文件必須等到前一個文件下載并執(zhí)行完成才會開始下載穗泵。
在這些文件"one by one"的下載執(zhí)行過程中,用戶看到的則是一片空白谜疤。
不是那么好的好消息
IE8/Firefox3.5/Safari 4/Chrome 2都允許并行下載js文件佃延,也就是說,script標簽在下載外部資源時茎截,不會阻塞其他的script標簽苇侵。
遺憾的是:
- js文件的下載過程仍然會阻塞其他資源的下載,比如圖片企锌。
- 頁面仍然必須等到所有js代碼下載并執(zhí)行完畢才能繼續(xù)渲染榆浓。
因此,盡管瀏覽器通過允許并行下載提高了性能撕攒,但腳本阻塞仍然是一個問題陡鹃。
綜上,推薦將所有的script標簽盡可能的放在body標簽的底部抖坪,以盡量減少對整個頁面渲染的影響萍鲸。
組織腳本
既然每個script標簽初始下載時都會阻塞頁面渲染,那么我們可以通過減少頁面上script標簽的數(shù)量來改善這一情況擦俐。不光是外鏈的腳本脊阴,內(nèi)嵌腳本的數(shù)量同樣也要限制(畢竟執(zhí)行js代碼也會阻塞頁面的渲染)。
多于外鏈的腳本蚯瞧,這里的情況有一點需要額外注意的地方:考慮HTTP請求會帶來額外的性能開銷嘿期,所以下載單個100kb的文件要比下載四個25kb的文件更快。從這個角度出發(fā)埋合,更能說明減少外鏈腳本文件的數(shù)量將會改善性能备徐。
What u should do?合并腳本甚颂!
無阻塞的腳本
隨著web應用的功能越豐富蜜猾,所需要的js代碼就越多秀菱,所以精簡源代碼也并不總是可行。盡管下載單個較大的js文件只產(chǎn)生一次HTTP請求蹭睡,但這樣做卻會鎖死瀏覽器一大段時間衍菱。
為了避免這種情況,需要向頁面中逐步加載js文件棠笑,這樣做梦碗,在某種程度上來說不會阻塞瀏覽器。
延遲的腳本
HTML4為script標簽定義了一個擴展屬性:defer蓖救。Defer表明本元素所含的腳本不會修改DOM洪规,因此代碼能安全的延遲執(zhí)行。
帶有defer屬性的script標簽可以放在文檔的任何位置(不會阻塞瀏覽器的其他進程循捺,此類文件可以與頁面中的其他資源并行下載)斩例,對應的js代碼將在頁面解析到script標簽時開始下載,但并不會執(zhí)行从橘,(onload事件被出發(fā)前)才會執(zhí)行念赶。
PS:截至這本書的第一版(2010年11月),這個屬性對IE和Firefox的支持性比較好(我的天居然有IE)恰力,如果真要投入到實際的項目中叉谜,不妨先去檢查一下瀏覽器的兼容性先~
動態(tài)腳本元素
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'js_file.js';
document.getElementsByTagName('body')[0].appendChild(script);
Firefox/Opera/Chrome和Safari 3+會在script元素接收完成時出發(fā)一個load事件,所以你可以通過監(jiān)聽這個事件來獲得腳本加載完成時的狀態(tài):
script.onload = function(){
console.log('script loaded.');
}
一向特立獨行的IE自然有他的另一套:觸發(fā)一個readystatechange事件踩萎。script元素提供一個readyState屬性停局,有以下五種取值:
- "uninitialized"
- "loading"
- "loaded"
- "interactive" 數(shù)據(jù)完成下載但尚不可用
- "complete"
所以,
script.onreadystatechange = function(){
if (script.readyState == 'loaded' || script.readyState == 'complete'){
script.onreadystatechange = null;
console.log('script loaded.');
}
}
至此香府,我們得到了一個可以應用于廣泛瀏覽器的動態(tài)加載腳本用的函數(shù):
function loadScript(url, callback){
var script = document.createElement('script');
script.type = 'text/javascript';
if (script.readyState){//IE
script.onreadystatechange = function(){
if (script.readyState == 'loaded' || script.readyState == 'complete'){
script.onreadystatechange = null;
callback();
}
}
}else{
script.onload = function(){
callback();
}
}
}
可以這么用:
loadScript('file.js',function(){
console.log('script loaded.');
});
也可以這么用:
loadScript('file_1.js',function(){
loadScript('file_2.js',function(){
loadScript('file_3.js',function(){
console.log('all files are loaded.');
});
});
});
XMLHttpRequest腳本注入
標題看起來很高大上的樣子董栽,其實就是Ajax。
var xhr = new XMLHttpRequest();
xhr.open('get','file.js',true);
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304){
var script = document.createElement('script');
script.type = 'text/javascript';
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
};
xhr.send(null);
局限:跨域問題企孩。
小結(jié)
- body閉合標簽之前锭碳,將所有的script標簽放到頁面底部。這樣能確保在腳本執(zhí)行前頁面已經(jīng)完成了渲染勿璃。
- 合并腳本擒抛。頁面中的script標簽越少,加載也就越快补疑,響應也更迅速闻葵。
- 無阻塞下載js:
- script標簽的defer屬性
- 動態(tài)創(chuàng)建script元素來下載并執(zhí)行代碼
- 使用XHR對象下載js代碼并注入頁面中。