前端性能優化之加載技術

在這個前端用戶體驗愈來愈重要的時代,你的頁面稍微有點卡頓,都難以挽留用戶。而做爲一名有追求的前端,勢必要力所能及地優化咱們前端頁面的性能。今天,就來談一談那些前端性能優化的加載技術,利用這些技術能夠很好地提升網站的響應速度和用戶體驗。javascript

頁面渲染

在理解真正的優化技術以前,咱們須要先了解爲何須要優化?這得從瀏覽器的渲染引擎談起。瀏覽器從獲取HTML文檔開始,就進入了渲染引擎的工做階段,其目的是將網頁的內容顯示在瀏覽器屏幕上。大致能夠描述爲從解析HTML內容,構造DOM節點再到DOM元素佈局定位最後再繪製DOM元素的這樣一個過程。更加詳細的內容能夠參考How browser works, 要看中文的童鞋能夠看這篇譯文css

在頁面渲染的這樣一個過程當中,有一個關鍵點是若是在解析內容的過程當中遇到了腳本標籤,如:<script src="example.js"></script>,瀏覽器就會暫停內容的解析,轉而開始下載腳本。而且只有等腳本下載完並執行結束後,渲染引擎纔會繼續解析。那麼這樣一來,頁面顯示的時間必然會被延長。所以咱們須要優化的點就是儘量地讓頁面更早地被渲染出來。html

腳本加載的優化

要解決上面說到的腳本加載問題,一般有三種解決方案:將腳本放在HTML末尾、動態加載腳本以及異步加載腳本。最經常使用的應該就是將全部腳本放置在HTML文檔的末尾了。這應該是每一個前端剛入門時,被教的最多的。對於這個方法,這裏就很少作介紹,直接上重頭戲。前端

動態加載

所謂動態加載腳本就是利用javascript代碼來加載腳本,一般是手工建立script元素,而後等到HTML文檔解析完畢後插入到文檔中去。這樣就能夠很好地控制腳本加載的時機,從而避免阻塞問題。html5

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屬性就能夠實現這種需求:java

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

雖然利用了這兩個屬性的script標籤均可以實現異步加載,同時不阻塞腳本解析。可是使用async屬性的腳本執行順序是不能獲得保證的。而使用defer屬性的腳本執行順序能夠獲得保證。另外一方面,defer屬性是在html文檔解析完成後,DOMContentLoaded事件以前就會執行js。async一旦加載完js後就會立刻執行,最遲不超過window.onload事件。因此,若是腳本沒有操做DOM等元素,或者與DOM時候加載完成無關,直接使用async腳本就好。若是須要DOM,就只能使用defer了。jquery

這裏介紹的兩種方法在實際運用過程當中須要權衡一下的,渲染速度變快也就意味着腳本加載時間會變長。webpack

解決異步加載腳本的問題

上面介紹的異步加載腳本並非十分完美的。如何處理加載過程當中這些腳本的互相依賴關係,就成了實現異步加載過程當中所須要考慮的問題。一方面,對於頁面中那些獨立的腳本,如用戶統計等插件就能夠放心大膽地使用異步加載。而另外一方面,對於那些確實須要處理依賴關係的腳本,業界已經有很成熟的解決方案了。如採用AMD規範的RequireJS,甚至有采用了hack技術(經過欺騙瀏覽器下載但不執行腳本)的labjs(已過期)。若是你熟悉promise的話,就知道這是在JS中處理異步的一種強有力的工具。下面以promise技術來實現處理異步腳本加載過程當中de的依賴問題:git

// 執行腳本
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'));複製代碼

能夠看到,咱們針對每一個腳本依賴都會建立一個promise對象來管理其狀態。採用動態插入腳本的方式來管理腳本,而後利用腳本onload和onreadystatechange(兼容性處理)事件來監聽腳本是否加載完成。一旦加載完畢,就會觸發promise的resovle方法。最後,針對依賴的處理,是promise的all方法,這個方法只有在全部promise對象都resolved的時候纔會觸發resolve方法,這樣一來,咱們就能夠確保在執行回調以前,全部依賴的腳本都已經加載並執行完畢。es6

懶加載(lazyload)

懶加載是一種按需加載的方式,也一般被稱爲延遲加載。主要思想是經過延遲相關資源的加載,從而提升頁面的加載和響應速度。在這裏主要介紹兩種實現懶加載的技術:虛擬代理技術以及惰性初始化技術。

虛擬代理加載

所謂虛擬代理加載,即爲真正加載的對象事先提供一個代理或者說佔位符。最多見的場景是在圖片的懶加載中,先用一種loading的圖片佔位,而後再用異步的方式加載圖片。等真正圖片加載完成後就填充進圖片節點中去。

// 頁面中的圖片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'));
          images[i].onload = () => images[i].removeAttribute('data-src');
      }
  }
}複製代碼

惰性初始化

惰性初始模式是在程序設計過程當中經常使用的一種設計模式。顧名思義,這個模式就是一種將代碼初始化的時機推遲(特別是那些初始化消耗較大的資源),從而來提高性能的技術。

jQuery中大名鼎鼎的ready方法就用到了這項技術,其目的是爲了在頁面DOM元素加載完成後就能夠作相應的操做,而不須要等待全部資源加載完畢後。與瀏覽器中原生的onload事件相比,能夠更加提早地介入對DOM的干涉。當頁面中包含大量圖片等資源時,這個方法就顯出它的好處了。在jQuery內部的實現原理上,它會設置一個標誌位來判斷頁面是否加載完畢,若是沒有加載完成,會將要執行的函數緩存起來。當頁面加載完畢後,再一一執行。這樣一來,就將本來應該立刻執行的代碼,延遲到頁面加載完畢後再執行。感興趣的能夠去閱讀這一部分的源碼,裏面還包括了瀏覽器兼容等處理。

選擇時機

選擇時機:比較常見的兩種

  1. 滾動條監聽
  1. 事件回調(須要用戶交互的地方)

固然,你也能夠根據具體的業務場景選擇延遲加載的時機。

滾動條監聽

滾動條監聽,經常用在大型圖片流等場景下。經過對用戶滾動結束的區域進行計算,從而只加載目標區域中的資源。這樣就能夠實現節流的目的。

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

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

        clearTimeout(timeout);
        // 若是達到了規定的觸發時間間隔,觸發 handler
        if(curTime - startTime >= mustRun){
            func.apply(context,args);
            startTime = curTime;
        // 沒達到觸發間隔,從新設定定時器
        }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');
// 採用了節流函數, 加載圖片
window.addEventListener('scroll',throttle(lazyLoadImgs(images),500,1000));複製代碼

事件回調

這種場景就是那些須要用戶交互的地方,如點擊加載更多之類的。這些資源每每經過在用戶交互的瞬間(如點擊一個觸發按鈕),發起ajax請求來獲取資源。比較簡單,在此再也不贅述。

利用webpack實現腳本加載優化

現現在,對於大型項目你們都會用上打包工具。現代化的工具使得咱們沒必要再寫那些又長又難懂的代碼。針對懶加載,webpack也提供了十分友好的支持。這裏主要介紹兩種方式。

import()方法

咱們知道,在原生es6的語法中,提供了import和export的方式來管理模塊。而其import關鍵字是被設置成靜態的,所以不支持動態綁定。不過在es6的stage 3規範中,引入了一個新的方法import()使得動態加載模塊成爲可能。因此,你能夠在項目中使用這樣的代碼:

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

//或者更優雅的寫法
$('#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時代官方提供的懶加載方案。如今,已經被import()語法取代了。爲了文章的完整性,這裏也作一些介紹。

在webpack編譯過程當中,會靜態地解析require.ensure中的模塊,並將其添加到一個單獨的chunk中,從而實現代碼的按需加載。

語法以下:

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

一個十分常見的例子是在寫單頁面應用的時候,使用該技術實現基於不一樣路由的按需加載:

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

預加載

首屏加載的問題解決後,用戶在具體的頁面使用過程當中的體驗也很重要。若是可以經過預判用戶的行爲,提早加載所須要的資源,則能夠快速地響應用戶的操做,從而打造更加良好的用戶體驗。另外一方面,經過提早發起網絡請求,也能夠減小因爲網絡過慢致使的用戶等待時間。所以,「預加載」的技術就閃亮登場了。

preload規範

preload 是w3c新出的一個標準。利用link的rel屬性來聲明相關「proload",從而實現預加載的目的。就像這樣:

<link rel="preload" href="example.js" as="script">複製代碼

其中rel屬性是用來告知瀏覽器啓用preload功能,而as屬性是用來明確須要預加載資源的類型,這個資源類型不只僅包括js腳本(script),還能夠是圖片(image),css(style),視頻(media)等等。瀏覽器檢測到這個屬性後,就會預先加載資源。

這個規範目前兼容性方面還不是很好,因此能夠先稍微瞭解一下。webpack如今也已經有相關的插件,若是感興趣的話,請移步preload-webpack-plugin。對於更加詳細的技術細節,這裏推薦一篇博客www.smashingmagazine.com/2016/02/pre…

DNS Prefetch 預解析

還有一個能夠優化網頁速度的方式是利用dns的預解析技術。同preload相似,DNS Prefetch在網絡層面上優化了資源加載的速度。咱們知道,針對DNS的前端優化,主要分爲減小DNS的請求次數,還有就是進行DNS預先獲取。DNS prefetch就是爲了實現這後者。其用法也很簡單,只要在link標籤上加上對應的屬性就好了。

<meta http-equiv="x-dns-prefetch-control" content="on" /> /* 這是用來告知瀏覽器當前頁面要作DNS預解析 */
<link rel="dns-prefetch" href="//example.com">複製代碼

在支持該標準的瀏覽器上,會自動對連接中的地址域名作DNS解析緩存。不過,像Goolge、火狐這樣的現代瀏覽器即便不設置這個屬性,也能在後臺作自動預解析。若是你的頁面中須要大量訪問不一樣域名的資源,能夠利用這項技術加快資源的獲取,從而得到更好的用戶體驗。須要注意的是,DNS預解析雖好,可是也不能濫用。若是對多頁面重複DNS預解析,會增長DNS的查詢次數。

總結

一般對於大型應用來講,完整加載全部javascript代碼是十分耗時的工做。所以,一般會將JavaScript分爲兩個部分(一部分是渲染初始化頁面所必須的,另外一部分則是剩下的腳本)來進行加載。這樣就能夠儘量快速地渲染出網頁。經過監聽onload事件,能夠很好地控制回調的時機,同時採用異步加載等技術可以同時並行加載多個腳本,從而大大提升最終頁面的渲染速度。最好是把在onload事件以前執行的代碼拆分紅一個單獨的文件。固然,在處理腳本加載這一過程當中還存在着幾個問題:1.如何找到須要拆分的代碼? 2 怎樣處理競爭狀態 ?3.如何延遲加載其他部分的代碼?但願這篇文章可以給你啓發!對於文中有錯漏之處,歡迎指出。鑑於本人水平有限,也歡迎你們來多多交流。

參考資料

《Javascript性能優化》

bubkoo.com/2015/11/19/…

2ality.com/2017/01/imp…

segmentfault.com/a/119000000…

perishablepress.com/3-ways-prel…

www.youtube.com/watch?v=wKC…

相關文章
相關標籤/搜索