在這個前端用戶體驗越來越重要的時代,你的頁面稍微有點卡頓坐梯,都難以挽留用戶。而作為一名有追求的前端刹帕,勢必要力所能及地優(yōu)化我們前端頁面的性能。今天谎替,就來談一談那些前端性能優(yōu)化的加載技術偷溺,利用這些技術可以很好地提高網(wǎng)站的響應速度和用戶體驗。
頁面渲染
在理解真正的優(yōu)化技術之前钱贯,我們需要先了解為什么需要優(yōu)化挫掏?這得從瀏覽器的渲染引擎談起。瀏覽器從獲取HTML文檔開始秩命,就進入了渲染引擎的工作階段尉共,其目的是將網(wǎng)頁的內容顯示在瀏覽器屏幕上。大體可以描述為從解析HTML內容弃锐,構造DOM節(jié)點再到DOM元素布局定位最后再繪制DOM元素的這樣一個過程袄友。更加詳細的內容可以參考How browser works, 要看中文的童鞋可以看這篇譯文。
在頁面渲染的這樣一個過程中霹菊,有一個關鍵點是如果在解析內容的過程中遇到了腳本標簽剧蚣,如:<script src="example.js"></script>
,瀏覽器就會暫停內容的解析旋廷,轉而開始下載腳本鸠按。并且只有等腳本下載完并執(zhí)行結束后,渲染引擎才會繼續(xù)解析饶碘。那么這樣一來目尖,頁面顯示的時間必然會被延長。因此我們需要優(yōu)化的點就是盡可能地讓頁面更早地被渲染出來扎运。
腳本加載的優(yōu)化
要解決上面說到的腳本加載問題瑟曲,通常有三種解決方案:將腳本放在HTML末尾饮戳、動態(tài)加載腳本以及異步加載腳本。最常用的應該就是將所有腳本放置在HTML文檔的末尾了测蹲。這應該是每個前端剛入門時莹捡,被教的最多的。對于這個方法扣甲,這里就不多做介紹篮赢,直接上重頭戲。
動態(tài)加載
所謂動態(tài)加載腳本就是利用javascript代碼來加載腳本琉挖,通常是手工創(chuàng)建script元素启泣,然后等到HTML文檔解析完畢后插入到文檔中去。這樣就可以很好地控制腳本加載的時機示辈,從而避免阻塞問題寥茫。
function loadJS(src) {
const script = document.createElement('script');
script.src = src;
document.getElementsByTagName('head')[0].appendChild(script);
}
loadJS('http://example.com/scq000.js');
異步加載
我們都知道,在計算機程序中同步的模式會產生阻塞問題矾麻。所以為了解決同步解析腳本會阻塞瀏覽器渲染的問題纱耻,采用異步加載腳本就成為了一種好的選擇。利用腳本的async和defer屬性就可以實現(xiàn)這種需求:
<script type="text/javascript" src="./a.js" async></script>
<script type="text/javascript" src="./b.js" defer></script>
雖然利用了這兩個屬性的script標簽都可以實現(xiàn)異步加載险耀,同時不阻塞腳本解析弄喘。但是使用async屬性的腳本執(zhí)行順序是不能得到保證的。而使用defer屬性的腳本執(zhí)行順序可以得到保證甩牺。另一方面蘑志,defer屬性是在html文檔解析完成后,DOMContentLoaded事件之前就會執(zhí)行js贬派。async一旦加載完js后就會馬上執(zhí)行急但,最遲不超過window.onload事件。所以搞乏,如果腳本沒有操作DOM等元素波桩,或者與DOM時候加載完成無關,直接使用async腳本就好请敦。如果需要DOM突委,就只能使用defer了。
這里介紹的兩種方法在實際運用過程中需要權衡一下的冬三,渲染速度變快也就意味著腳本加載時間會變長匀油。
解決異步加載腳本的問題
上面介紹的異步加載腳本并不是十分完美的。如何處理加載過程中這些腳本的互相依賴關系勾笆,就成了實現(xiàn)異步加載過程中所需要考慮的問題敌蚜。一方面,對于頁面中那些獨立的腳本窝爪,如用戶統(tǒng)計等插件就可以放心大膽地使用異步加載弛车。而另一方面齐媒,對于那些確實需要處理依賴關系的腳本,業(yè)界已經有很成熟的解決方案了纷跛。如采用AMD規(guī)范的RequireJS,甚至有采用了hack技術(通過欺騙瀏覽器下載但不執(zhí)行腳本)的labjs(已過時)喻括。如果你熟悉promise的話,就知道這是在JS中處理異步的一種強有力的工具贫奠。下面以promise技術來實現(xiàn)處理異步腳本加載過程中de的依賴問題:
// 執(zhí)行腳本
function exec(src) {
const script = document.createElement('script');
script.src = src;
// 返回一個獨立的promise
return new Promise((resolve, reject) => {
var done = false;
script.onload = script.onreadystatechange = () => {
if (!done && (!script.readyState || script.readyState === "loaded" || script.readyState === "complete")) {
done = true;
// 避免內存泄漏
script.onload = script.onreadystatechange = null;
resolve(script);
}
}
script.onerror = reject;
document.getElementsByTagName('head')[0].appendChild(script);
});
}
function asyncLoadJS(dependencies) {
return Promise.all(dependencies.map(exec));
}
asyncLoadJS(['https://code.jquery.com/jquery-2.2.1.js', 'https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js']).then(() => console.log('all done'));
可以看到唬血,我們針對每個腳本依賴都會創(chuàng)建一個promise對象來管理其狀態(tài)。采用動態(tài)插入腳本的方式來管理腳本唤崭,然后利用腳本onload和onreadystatechange(兼容性處理)事件來監(jiān)聽腳本是否加載完成拷恨。一旦加載完畢,就會觸發(fā)promise的resovle方法谢肾。最后腕侄,針對依賴的處理,是promise的all方法芦疏,這個方法只有在所有promise對象都resolved的時候才會觸發(fā)resolve方法冕杠,這樣一來,我們就可以確保在執(zhí)行回調之前酸茴,所有依賴的腳本都已經加載并執(zhí)行完畢拌汇。
懶加載(lazyload)
懶加載是一種按需加載的方式,也通常被稱為延遲加載弊决。主要思想是通過延遲相關資源的加載,從而提高頁面的加載和響應速度魁淳。在這里主要介紹兩種實現(xiàn)懶加載的技術:虛擬代理技術以及惰性初始化技術飘诗。
虛擬代理加載
所謂虛擬代理加載,即為真正加載的對象事先提供一個代理或者說占位符界逛。最常見的場景是在圖片的懶加載中昆稿,先用一種loading的圖片占位,然后再用異步的方式加載圖片息拜。等真正圖片加載完成后就填充進圖片節(jié)點中去溉潭。
// 頁面中的圖片url事先先存在其data-src屬性上
const lazyLoadImg = function() {
const images = document.getElementsByTagName('img');
for(let i = 0; i < images.length; i++) {
if(images[i].getAttribute('data-src')) {
images[i].setAttribute('src', images[i].getAttribute('data-src'));
img.onload = () => img.removeAttribute('data-src');
}
}
}
惰性初始化
惰性初始模式是在程序設計過程中常用的一種設計模式。顧名思義少欺,這個模式就是一種將代碼初始化的時機推遲(特別是那些初始化消耗較大的資源)喳瓣,從而來提升性能的技術。
jQuery中大名鼎鼎的ready方法就用到了這項技術赞别,其目的是為了在頁面DOM元素加載完成后就可以做相應的操作畏陕,而不需要等待所有資源加載完畢后。與瀏覽器中原生的onload事件相比仿滔,可以更加提前地介入對DOM的干涉惠毁。當頁面中包含大量圖片等資源時犹芹,這個方法就顯出它的好處了。在jQuery內部的實現(xiàn)原理上鞠绰,它會設置一個標志位來判斷頁面是否加載完畢腰埂,如果沒有加載完成,會將要執(zhí)行的函數(shù)緩存起來蜈膨。當頁面加載完畢后屿笼,再一一執(zhí)行。這樣一來丈挟,就將原本應該馬上執(zhí)行的代碼刁卜,延遲到頁面加載完畢后再執(zhí)行。感興趣的可以去閱讀這一部分的源碼曙咽,里面還包括了瀏覽器兼容等處理蛔趴。
選擇時機
選擇時機:比較常見的兩種
- 滾動條監(jiān)聽
- 事件回調(需要用戶交互的地方)
當然,你也可以根據(jù)具體的業(yè)務場景選擇延遲加載的時機例朱。
滾動條監(jiān)聽
滾動條監(jiān)聽孝情,常常用在大型圖片流等場景下。通過對用戶滾動結束的區(qū)域進行計算洒嗤,從而只加載目標區(qū)域中的資源箫荡。這樣就可以實現(xiàn)節(jié)流的目的。
// 簡單的節(jié)流函數(shù)
function throttle(func, wait, mustRun) {
var timeout,
startTime = new Date();
return function() {
var context = this,
args = arguments,
curTime = new Date();
clearTimeout(timeout);
// 如果達到了規(guī)定的觸發(fā)時間間隔渔隶,觸發(fā) handler
if(curTime - startTime >= mustRun){
func.apply(context,args);
startTime = curTime;
// 沒達到觸發(fā)間隔羔挡,重新設定定時器
}else{
timeout = setTimeout(func, wait);
}
};
};
// 判斷元素是否在可視范圍內
function elementInViewport(element) {
const rect = element.getBoundingClientRect();
return (rect.top >= 0 && rect.left >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight));
}
function lazyLoadImgs() {
const count = 0;
return function() {
[].slice.call(images, count).forEach(image => {
if(elementInViewport(elementInViewport(image))) {
image.setAttribute('src', image.getAttribute('data-src'));
count++;
}
});
}
}
const images = document.getElementByTagName('img');
// 采用了節(jié)流函數(shù), 加載圖片
window.addEventListener('scroll',throttle(lazyLoadImgs(images),500,1000));
事件回調
這種場景就是那些需要用戶交互的地方,如點擊加載更多之類的间唉。這些資源往往通過在用戶交互的瞬間(如點擊一個觸發(fā)按鈕)绞灼,發(fā)起ajax請求來獲取資源。比較簡單呈野,在此不再贅述低矮。
利用webpack實現(xiàn)腳本加載優(yōu)化
現(xiàn)如今,對于大型項目大家都會用上打包工具”幻埃現(xiàn)代化的工具使得我們不必再寫那些又長又難懂的代碼军掂。針對懶加載,webpack也提供了十分友好的支持昨悼。這里主要介紹兩種方式蝗锥。
import()方法
我們知道,在原生es6的語法中率触,提供了import和export的方式來管理模塊玛追。而其import關鍵字是被設置成靜態(tài)的,因此不支持動態(tài)綁定。不過在es6的stage 3規(guī)范中痊剖,引入了一個新的方法import()
使得動態(tài)加載模塊成為可能韩玩。所以,你可以在項目中使用這樣的代碼:
$('#button').click(function() {
import('./dialog.js')
.then(dialog => {
//do something
})
.catch(err => {
console.log('模塊加載錯誤');
});
});
//或者更優(yōu)雅的寫法
$('#button').click(async function() {
const dialog = await import('./dialog.js');
//do something with dialog
});
由于該語法是基于promise的陆馁,所以如果需要兼容舊瀏覽器找颓,請確保在項目中使用es6-promise或者promise-polyfill。同時叮贩,如果使用的是babel击狮,需要添加syntax-dynamic-import插件。
require.ensure
require.ensure與import()類似益老,同樣也是基于promise的異步加載模塊的一種方法彪蓬。這是在webpack 1.x時代官方提供的懶加載方案。現(xiàn)在捺萌,已經被import()語法取代了档冬。為了文章的完整性,這里也做一些介紹桃纯。
在webpack編譯過程中酷誓,會靜態(tài)地解析require.ensure中的模塊,并將其添加到一個單獨的chunk中态坦,從而實現(xiàn)代碼的按需加載盐数。
語法如下:
require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)
一個十分常見的例子是在寫單頁面應用的時候,使用該技術實現(xiàn)基于不同路由的按需加載:
const routes = [
{path: '/comment', component: r => require.ensure([], r(require('./Comment')), 'comment')}
];
預加載
首屏加載的問題解決后伞梯,用戶在具體的頁面使用過程中的體驗也很重要玫氢。如果能夠通過預判用戶的行為,提前加載所需要的資源谜诫,則可以快速地響應用戶的操作历涝,從而打造更加良好的用戶體驗惊完。另一方面厢汹,通過提前發(fā)起網(wǎng)絡請求霹俺,也可以減少由于網(wǎng)絡過慢導致的用戶等待時間敬特。因此掰邢,“預加載”的技術就閃亮登場了。
preload規(guī)范
preload 是w3c新出的一個標準伟阔。利用link的rel屬性來聲明相關“proload"辣之,從而實現(xiàn)預加載的目的。就像這樣:
<link rel="preload" href="example.js" as="script">
其中rel屬性是用來告知瀏覽器啟用preload功能皱炉,而as屬性是用來明確需要預加載資源的類型怀估,這個資源類型不僅僅包括js腳本(script),還可以是圖片(image),css(style)多搀,視頻(media)等等歧蕉。瀏覽器檢測到這個屬性后,就會預先加載資源康铭。
這個規(guī)范目前兼容性方面還不是很好惯退,所以可以先稍微了解一下。webpack現(xiàn)在也已經有相關的插件从藤,如果感興趣的話催跪,請移步preload-webpack-plugin。對于更加詳細的技術細節(jié)夷野,這里推薦一篇博客https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/懊蒸。
DNS Prefetch 預解析
還有一個可以優(yōu)化網(wǎng)頁速度的方式是利用dns的預解析技術。同preload類似悯搔,DNS Prefetch在網(wǎng)絡層面上優(yōu)化了資源加載的速度骑丸。我們知道,針對DNS的前端優(yōu)化鳖孤,主要分為減少DNS的請求次數(shù)者娱,還有就是進行DNS預先獲取。DNS prefetch就是為了實現(xiàn)這后者苏揣。其用法也很簡單黄鳍,只要在link標簽上加上對應的屬性就行了。
<meta http-equiv="x-dns-prefetch-control" content="on" /> /* 這是用來告知瀏覽器當前頁面要做DNS預解析 */
<link rel="dns-prefetch" >
在支持該標準的瀏覽器上平匈,會自動對鏈接中的地址域名做DNS解析緩存框沟。不過,像Goolge增炭、火狐這樣的現(xiàn)代瀏覽器即使不設置這個屬性忍燥,也能在后臺做自動預解析。如果你的頁面中需要大量訪問不同域名的資源隙姿,可以利用這項技術加快資源的獲取梅垄,從而獲得更好的用戶體驗。需要注意的是输玷,DNS預解析雖好队丝,但是也不能濫用。如果對多頁面重復DNS預解析欲鹏,會增加DNS的查詢次數(shù)机久。
總結
通常對于大型應用來說,完整加載所有javascript代碼是十分耗時的工作赔嚎。因此膘盖,通常會將JavaScript分為兩個部分(一部分是渲染初始化頁面所必須的胧弛,另一部分則是剩下的腳本)來進行加載。這樣就可以盡可能快速地渲染出網(wǎng)頁侠畔。通過監(jiān)聽onload事件结缚,可以很好地控制回調的時機,同時采用異步加載等技術能夠同時并行加載多個腳本践图,從而大大提高最終頁面的渲染速度掺冠。最好是把在onload事件之前執(zhí)行的代碼拆分成一個單獨的文件。當然码党,在處理腳本加載這一過程中還存在著幾個問題:1.如何找到需要拆分的代碼德崭? 2 怎樣處理競爭狀態(tài) ?3.如何延遲加載其余部分的代碼揖盘?希望這篇文章能夠給你啟發(fā)眉厨!對于文中有錯漏之處,歡迎指出兽狭。鑒于本人水平有限憾股,也歡迎大家來多多交流。
參考資料
《Javascript性能優(yōu)化》
http://bubkoo.com/2015/11/19/prefetching-preloading-prebrowsing/
http://2ality.com/2017/01/import-operator.html
https://segmentfault.com/a/1190000000684923
https://perishablepress.com/3-ways-preload-images-css-javascript-ajax/