前端性能優(yōu)化之加載技術(shù)

在這個(gè)前端用戶體驗(yàn)越來(lái)越重要的時(shí)代,你的頁(yè)面稍微有點(diǎn)卡頓,都難以挽留用戶。而作為一名有追求的前端,勢(shì)必要力所能及地優(yōu)化我們前端頁(yè)面的性能。今天,就來(lái)談一談那些前端性能優(yōu)化的加載技術(shù),利用這些技術(shù)可以很好地提高網(wǎng)站的響應(yīng)速度和用戶體驗(yàn)。

頁(yè)面渲染

在理解真正的優(yōu)化技術(shù)之前,我們需要先了解為什么需要優(yōu)化?這得從瀏覽器的渲染引擎談起。瀏覽器從獲取HTML文檔開(kāi)始,就進(jìn)入了渲染引擎的工作階段,其目的是將網(wǎng)頁(yè)的內(nèi)容顯示在瀏覽器屏幕上。大體可以描述為從解析HTML內(nèi)容,構(gòu)造DOM節(jié)點(diǎn)再到DOM元素布局定位最后再繪制DOM元素的這樣一個(gè)過(guò)程。更加詳細(xì)的內(nèi)容可以參考How browser works, 要看中文的童鞋可以看這篇譯文。

在頁(yè)面渲染的這樣一個(gè)過(guò)程中,有一個(gè)關(guān)鍵點(diǎn)是如果在解析內(nèi)容的過(guò)程中遇到了腳本標(biāo)簽,如:<script src="example.js"></script>,瀏覽器就會(huì)暫停內(nèi)容的解析,轉(zhuǎn)而開(kāi)始下載腳本。并且只有等腳本下載完并執(zhí)行結(jié)束后,渲染引擎才會(huì)繼續(xù)解析。那么這樣一來(lái),頁(yè)面顯示的時(shí)間必然會(huì)被延長(zhǎng)。因此我們需要優(yōu)化的點(diǎn)就是盡可能地讓頁(yè)面更早地被渲染出來(lái)。

腳本加載的優(yōu)化

要解決上面說(shuō)到的腳本加載問(wèn)題,通常有三種解決方案:將腳本放在HTML末尾、動(dòng)態(tài)加載腳本以及異步加載腳本。最常用的應(yīng)該就是將所有腳本放置在HTML文檔的末尾了。這應(yīng)該是每個(gè)前端剛?cè)腴T時(shí),被教的最多的。對(duì)于這個(gè)方法,這里就不多做介紹,直接上重頭戲。

動(dòng)態(tài)加載

所謂動(dòng)態(tài)加載腳本就是利用javascript代碼來(lái)加載腳本,通常是手工創(chuàng)建script元素,然后等到HTML文檔解析完畢后插入到文檔中去。這樣就可以很好地控制腳本加載的時(shí)機(jī),從而避免阻塞問(wèn)題。

function loadJS(src) {
  const script = document.createElement('script');
  script.src = src;
  document.getElementsByTagName('head')[0].appendChild(script);
}
loadJS('http://example.com/scq000.js');

異步加載

我們都知道,在計(jì)算機(jī)程序中同步的模式會(huì)產(chǎn)生阻塞問(wèn)題。所以為了解決同步解析腳本會(huì)阻塞瀏覽器渲染的問(wèn)題,采用異步加載腳本就成為了一種好的選擇。利用腳本的async和defer屬性就可以實(shí)現(xiàn)這種需求:

<script type="text/javascript" src="./a.js" async></script>
<script type="text/javascript" src="./b.js" defer></script>

雖然利用了這兩個(gè)屬性的script標(biāo)簽都可以實(shí)現(xiàn)異步加載,同時(shí)不阻塞腳本解析。但是使用async屬性的腳本執(zhí)行順序是不能得到保證的。而使用defer屬性的腳本執(zhí)行順序可以得到保證。另一方面,defer屬性是在html文檔解析完成后,DOMContentLoaded事件之前就會(huì)執(zhí)行js。async一旦加載完js后就會(huì)馬上執(zhí)行,最遲不超過(guò)window.onload事件。所以,如果腳本沒(méi)有操作DOM等元素,或者與DOM時(shí)候加載完成無(wú)關(guān),直接使用async腳本就好。如果需要DOM,就只能使用defer了。

這里介紹的兩種方法在實(shí)際運(yùn)用過(guò)程中需要權(quán)衡一下的,渲染速度變快也就意味著腳本加載時(shí)間會(huì)變長(zhǎng)。

解決異步加載腳本的問(wèn)題

上面介紹的異步加載腳本并不是十分完美的。如何處理加載過(guò)程中這些腳本的互相依賴關(guān)系,就成了實(shí)現(xiàn)異步加載過(guò)程中所需要考慮的問(wèn)題。一方面,對(duì)于頁(yè)面中那些獨(dú)立的腳本,如用戶統(tǒng)計(jì)等插件就可以放心大膽地使用異步加載。而另一方面,對(duì)于那些確實(shí)需要處理依賴關(guān)系的腳本,業(yè)界已經(jīng)有很成熟的解決方案了。如采用AMD規(guī)范的RequireJS,甚至有采用了hack技術(shù)(通過(guò)欺騙瀏覽器下載但不執(zhí)行腳本)的labjs(已過(guò)時(shí))。如果你熟悉promise的話,就知道這是在JS中處理異步的一種強(qiáng)有力的工具。下面以promise技術(shù)來(lái)實(shí)現(xiàn)處理異步腳本加載過(guò)程中de的依賴問(wèn)題:

// 執(zhí)行腳本
function exec(src) {
    const script = document.createElement('script');
    script.src = src;
    
    // 返回一個(gè)獨(dú)立的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;

              // 避免內(nèi)存泄漏
              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'));

可以看到,我們針對(duì)每個(gè)腳本依賴都會(huì)創(chuàng)建一個(gè)promise對(duì)象來(lái)管理其狀態(tài)。采用動(dòng)態(tài)插入腳本的方式來(lái)管理腳本,然后利用腳本onload和onreadystatechange(兼容性處理)事件來(lái)監(jiān)聽(tīng)腳本是否加載完成。一旦加載完畢,就會(huì)觸發(fā)promise的resovle方法。最后,針對(duì)依賴的處理,是promise的all方法,這個(gè)方法只有在所有promise對(duì)象都resolved的時(shí)候才會(huì)觸發(fā)resolve方法,這樣一來(lái),我們就可以確保在執(zhí)行回調(diào)之前,所有依賴的腳本都已經(jīng)加載并執(zhí)行完畢。

懶加載(lazyload)

懶加載是一種按需加載的方式,也通常被稱為延遲加載。主要思想是通過(guò)延遲相關(guān)資源的加載,從而提高頁(yè)面的加載和響應(yīng)速度。在這里主要介紹兩種實(shí)現(xiàn)懶加載的技術(shù):虛擬代理技術(shù)以及惰性初始化技術(shù)。

虛擬代理加載

所謂虛擬代理加載,即為真正加載的對(duì)象事先提供一個(gè)代理或者說(shuō)占位符。最常見(jiàn)的場(chǎng)景是在圖片的懶加載中,先用一種loading的圖片占位,然后再用異步的方式加載圖片。等真正圖片加載完成后就填充進(jìn)圖片節(jié)點(diǎn)中去。

// 頁(yè)面中的圖片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');
      }
  }
}

惰性初始化

惰性初始模式是在程序設(shè)計(jì)過(guò)程中常用的一種設(shè)計(jì)模式。顧名思義,這個(gè)模式就是一種將代碼初始化的時(shí)機(jī)推遲(特別是那些初始化消耗較大的資源),從而來(lái)提升性能的技術(shù)。

jQuery中大名鼎鼎的ready方法就用到了這項(xiàng)技術(shù),其目的是為了在頁(yè)面DOM元素加載完成后就可以做相應(yīng)的操作,而不需要等待所有資源加載完畢后。與瀏覽器中原生的onload事件相比,可以更加提前地介入對(duì)DOM的干涉。當(dāng)頁(yè)面中包含大量圖片等資源時(shí),這個(gè)方法就顯出它的好處了。在jQuery內(nèi)部的實(shí)現(xiàn)原理上,它會(huì)設(shè)置一個(gè)標(biāo)志位來(lái)判斷頁(yè)面是否加載完畢,如果沒(méi)有加載完成,會(huì)將要執(zhí)行的函數(shù)緩存起來(lái)。當(dāng)頁(yè)面加載完畢后,再一一執(zhí)行。這樣一來(lái),就將原本應(yīng)該馬上執(zhí)行的代碼,延遲到頁(yè)面加載完畢后再執(zhí)行。感興趣的可以去閱讀這一部分的源碼,里面還包括了瀏覽器兼容等處理。

選擇時(shí)機(jī)

選擇時(shí)機(jī):比較常見(jiàn)的兩種

  1. 滾動(dòng)條監(jiān)聽(tīng)
  1. 事件回調(diào)(需要用戶交互的地方)

當(dāng)然,你也可以根據(jù)具體的業(yè)務(wù)場(chǎng)景選擇延遲加載的時(shí)機(jī)。

滾動(dòng)條監(jiān)聽(tīng)

滾動(dòng)條監(jiān)聽(tīng),常常用在大型圖片流等場(chǎng)景下。通過(guò)對(duì)用戶滾動(dòng)結(jié)束的區(qū)域進(jìn)行計(jì)算,從而只加載目標(biāo)區(qū)域中的資源。這樣就可以實(shí)現(xiàn)節(jié)流的目的。


// 簡(jiǎn)單的節(jié)流函數(shù)
function throttle(func, wait, mustRun) {
    var timeout,
        startTime = new Date();

    return function() {
        var context = this,
            args = arguments,
            curTime = new Date();

        clearTimeout(timeout);
        // 如果達(dá)到了規(guī)定的觸發(fā)時(shí)間間隔,觸發(fā) handler
        if(curTime - startTime >= mustRun){
            func.apply(context,args);
            startTime = curTime;
        // 沒(méi)達(dá)到觸發(fā)間隔,重新設(shè)定定時(shí)器
        }else{
            timeout = setTimeout(func, wait);
        }
    };
};

// 判斷元素是否在可視范圍內(nèi)
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));

事件回調(diào)

這種場(chǎng)景就是那些需要用戶交互的地方,如點(diǎn)擊加載更多之類的。這些資源往往通過(guò)在用戶交互的瞬間(如點(diǎn)擊一個(gè)觸發(fā)按鈕),發(fā)起ajax請(qǐng)求來(lái)獲取資源。比較簡(jiǎn)單,在此不再贅述。

利用webpack實(shí)現(xiàn)腳本加載優(yōu)化

現(xiàn)如今,對(duì)于大型項(xiàng)目大家都會(huì)用上打包工具。現(xiàn)代化的工具使得我們不必再寫那些又長(zhǎng)又難懂的代碼。針對(duì)懶加載,webpack也提供了十分友好的支持。這里主要介紹兩種方式。

import()方法

我們知道,在原生es6的語(yǔ)法中,提供了import和export的方式來(lái)管理模塊。而其import關(guān)鍵字是被設(shè)置成靜態(tài)的,因此不支持動(dòng)態(tài)綁定。不過(guò)在es6的stage 3規(guī)范中,引入了一個(gè)新的方法import()使得動(dòng)態(tài)加載模塊成為可能。所以,你可以在項(xiàng)目中使用這樣的代碼:

$('#button').click(function() {
  import('./dialog.js')
    .then(dialog => {
        //do something
    })
    .catch(err => {
        console.log('模塊加載錯(cuò)誤');
    });
});

//或者更優(yōu)雅的寫法
$('#button').click(async function() {
    const dialog = await import('./dialog.js');
  //do something with dialog
  
});

由于該語(yǔ)法是基于promise的,所以如果需要兼容舊瀏覽器,請(qǐng)確保在項(xiàng)目中使用es6-promise或者promise-polyfill。同時(shí),如果使用的是babel,需要添加syntax-dynamic-import插件。

require.ensure

require.ensure與import()類似,同樣也是基于promise的異步加載模塊的一種方法。這是在webpack 1.x時(shí)代官方提供的懶加載方案。現(xiàn)在,已經(jīng)被import()語(yǔ)法取代了。為了文章的完整性,這里也做一些介紹。

在webpack編譯過(guò)程中,會(huì)靜態(tài)地解析require.ensure中的模塊,并將其添加到一個(gè)單獨(dú)的chunk中,從而實(shí)現(xiàn)代碼的按需加載。

語(yǔ)法如下:

require.ensure(dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)

一個(gè)十分常見(jiàn)的例子是在寫單頁(yè)面應(yīng)用的時(shí)候,使用該技術(shù)實(shí)現(xiàn)基于不同路由的按需加載:

const routes = [
    {path: '/comment', component: r => require.ensure([], r(require('./Comment')), 'comment')}
];

預(yù)加載

首屏加載的問(wèn)題解決后,用戶在具體的頁(yè)面使用過(guò)程中的體驗(yàn)也很重要。如果能夠通過(guò)預(yù)判用戶的行為,提前加載所需要的資源,則可以快速地響應(yīng)用戶的操作,從而打造更加良好的用戶體驗(yàn)。另一方面,通過(guò)提前發(fā)起網(wǎng)絡(luò)請(qǐng)求,也可以減少由于網(wǎng)絡(luò)過(guò)慢導(dǎo)致的用戶等待時(shí)間。因此,“預(yù)加載”的技術(shù)就閃亮登場(chǎng)了。

preload規(guī)范

preload 是w3c新出的一個(gè)標(biāo)準(zhǔn)。利用link的rel屬性來(lái)聲明相關(guān)“proload",從而實(shí)現(xiàn)預(yù)加載的目的。就像這樣:

<link rel="preload" href="example.js" as="script">

其中rel屬性是用來(lái)告知瀏覽器啟用preload功能,而as屬性是用來(lái)明確需要預(yù)加載資源的類型,這個(gè)資源類型不僅僅包括js腳本(script),還可以是圖片(image),css(style),視頻(media)等等。瀏覽器檢測(cè)到這個(gè)屬性后,就會(huì)預(yù)先加載資源。

這個(gè)規(guī)范目前兼容性方面還不是很好,所以可以先稍微了解一下。webpack現(xiàn)在也已經(jīng)有相關(guān)的插件,如果感興趣的話,請(qǐng)移步preload-webpack-plugin。對(duì)于更加詳細(xì)的技術(shù)細(xì)節(jié),這里推薦一篇博客https://www.smashingmagazine.com/2016/02/preload-what-is-it-good-for/

DNS Prefetch 預(yù)解析

還有一個(gè)可以優(yōu)化網(wǎng)頁(yè)速度的方式是利用dns的預(yù)解析技術(shù)。同preload類似,DNS Prefetch在網(wǎng)絡(luò)層面上優(yōu)化了資源加載的速度。我們知道,針對(duì)DNS的前端優(yōu)化,主要分為減少DNS的請(qǐng)求次數(shù),還有就是進(jìn)行DNS預(yù)先獲取。DNS prefetch就是為了實(shí)現(xiàn)這后者。其用法也很簡(jiǎn)單,只要在link標(biāo)簽上加上對(duì)應(yīng)的屬性就行了。

<meta http-equiv="x-dns-prefetch-control" content="on" /> /* 這是用來(lái)告知瀏覽器當(dāng)前頁(yè)面要做DNS預(yù)解析 */
<link rel="dns-prefetch" >

在支持該標(biāo)準(zhǔn)的瀏覽器上,會(huì)自動(dòng)對(duì)鏈接中的地址域名做DNS解析緩存。不過(guò),像Goolge、火狐這樣的現(xiàn)代瀏覽器即使不設(shè)置這個(gè)屬性,也能在后臺(tái)做自動(dòng)預(yù)解析。如果你的頁(yè)面中需要大量訪問(wèn)不同域名的資源,可以利用這項(xiàng)技術(shù)加快資源的獲取,從而獲得更好的用戶體驗(yàn)。需要注意的是,DNS預(yù)解析雖好,但是也不能濫用。如果對(duì)多頁(yè)面重復(fù)DNS預(yù)解析,會(huì)增加DNS的查詢次數(shù)。

總結(jié)

通常對(duì)于大型應(yīng)用來(lái)說(shuō),完整加載所有javascript代碼是十分耗時(shí)的工作。因此,通常會(huì)將JavaScript分為兩個(gè)部分(一部分是渲染初始化頁(yè)面所必須的,另一部分則是剩下的腳本)來(lái)進(jìn)行加載。這樣就可以盡可能快速地渲染出網(wǎng)頁(yè)。通過(guò)監(jiān)聽(tīng)onload事件,可以很好地控制回調(diào)的時(shí)機(jī),同時(shí)采用異步加載等技術(shù)能夠同時(shí)并行加載多個(gè)腳本,從而大大提高最終頁(yè)面的渲染速度。最好是把在onload事件之前執(zhí)行的代碼拆分成一個(gè)單獨(dú)的文件。當(dāng)然,在處理腳本加載這一過(guò)程中還存在著幾個(gè)問(wèn)題:1.如何找到需要拆分的代碼? 2 怎樣處理競(jìng)爭(zhēng)狀態(tài) ?3.如何延遲加載其余部分的代碼?希望這篇文章能夠給你啟發(fā)!對(duì)于文中有錯(cuò)漏之處,歡迎指出。鑒于本人水平有限,也歡迎大家來(lái)多多交流。

參考資料

《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/

https://www.youtube.com/watch?v=wKCBFpia-bI&t=669s

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 在這個(gè)前端用戶體驗(yàn)越來(lái)越重要的時(shí)代,你的頁(yè)面稍微有點(diǎn)卡頓,都難以挽留用戶。而作為一名有追求的前端,勢(shì)必要力所能及地...
    程序員小布的養(yǎng)生之道閱讀 1,323評(píng)論 0 50
  • 圍繞前端的性能多如牛毛,涉及到方方面面,以我我們將圍繞PC瀏覽器和移動(dòng)端瀏覽器的優(yōu)化策略進(jìn)行羅列注意,是羅列不是展...
    流動(dòng)碼文閱讀 744評(píng)論 0 0
  • 網(wǎng)站優(yōu)化離不開(kāi)前后端的互相協(xié)作,但是對(duì)于前端工程師來(lái)說(shuō),在保證后端技術(shù)方案不變時(shí),能不能只利用前端技術(shù)來(lái)優(yōu)化網(wǎng)站呢...
    留七七閱讀 6,585評(píng)論 0 31
  • 在線閱讀 http://interview.poetries.top[http://interview.poetr...
    前端進(jìn)階之旅閱讀 115,569評(píng)論 24 450
  • 上班路,被雨水澆了。 今天什么節(jié)氣? 路邊手抓餅攤錯(cuò)失了我這筆生意。不太喜歡手抓餅,油膩。格調(diào)還是煎餅...
    126號(hào)225閱讀 124評(píng)論 0 0

友情鏈接更多精彩內(nèi)容