頁面性能優化

1、常規優化手段

白屏 -> 首次渲染

- loading:
    - 在頁面最前面加loading相關的html和css
    - 結合html-webpack-plugin插入loading(html+css)
    - prerender-spa-plugin(`暫記`)
    - 內聯CSS(`不考慮緩存策略的話`)
- 骨架屏<br/>
    Q: css文件的加載阻塞骨架屏的渲染<BR/>
    A:使用preload,僞代碼以下:
    ![](https://user-gold-cdn.xitu.io/2019/8/16/16c9923d9d085200?w=758&h=429&f=png&s=205515)
複製代碼

緩存策略

精簡打包的代碼

動態polyfill

  • polyfill.io:根據瀏覽器User-Agent頭,判斷其支持的特性,返回合適的polyfill。

webpack相關優化

  • Tree Shaking
    • 關閉babel的模塊處理:modules: false; pachage.json配置sideEffects
    • 對於lodash、underscore這樣的工具庫來講尤爲重要,開啓了這個特性後:
      import { capitalize } from 'lodash-es';
      複製代碼
      • 效果
      import {a} from xx 
      轉換爲 
      import {a} from 'xx/a'
      複製代碼
      • 原理
        // a.js
        export function a() {}
        // b.js
        export function b(){}
        // package/index.js
        import a from './a'
        import b from './b'
        export { a, b }
        // app.js
        import {a} from 'package'
        console.log(a)
        複製代碼
        結果treeShaking後:
        // a.js
        export function a() {}
        // b.js 再也不導出 function b(){}
        function b() {}
        // package/index.js 再也不導出 b 模塊
        import a from './a'
        import b from './b'
        export { a }
        // app.js
        import {a} from 'package'
        console.log(a)
        複製代碼
        配合 webpack 的 scope hoistinguglify 以後,b 模塊的痕跡會被徹底抹殺掉。
      • 反作用
        若是 b 模塊中添加了一些反作用,好比一個簡單的 log:
        // b.js
        export function b(v) { reutrn v }
        console.log(b(1))
        複製代碼
        處理後 b 模塊內容變成了:
        // b.js
        console.log(function (v){return v}(1))
        複製代碼
        注意:b文件中保留了console的代碼。
      • sideEffects 設置爲false,即無論它是否真的有反作用,只要它沒有被引用到,整個模塊/包都會被完整的移除。(上面的b文件被移除)
      • 注意事項: shim或者polyfill慎用!
        不是導出使用的模塊需聲明:
        "sideEffects": [
            "*.css",
            "src/javascript/base/da.js"
          ]
        複製代碼
  • splitChunks
  • import()

可交互 -> 內容加載完成

  • 懶加載
    • 監聽scroll事件
    • Intersection Observer獲取元素可見性
  • placeholder(提早佔位)
    和骨架屏不一樣。其解決的問題:文本圖片加載完先後,因爲高度被撐開,致使閃屏的現象。三方組件:react-placeholderreact-hold

直出HTML(同構)優化 (緩存時注意拉取CGI接口的參數處理)

  • 方案
    • 接口動靜分離 & Redis緩存(node層緩存html)
      • 靜態數據在node層獲取而後渲染;動態數據前端拉取並渲染
    • PWA直出優化(前端緩存html)
  • 同構直出的容災策略
    • 前端渲染和直出頁面的訪問路徑不一樣
    • 直出出錯時,轉發到前端渲染路徑

PWA

  • 核心:Web App Manifest,Service Worker,Push API & Notification API,App Shell & App Skeleton

Service Worker

  • 基於HTTPS
  • 大部分API都是基於promise-based
  • 運行在獨立的worker進程(webworker)
  • 離線緩存
  • 弱網快速訪問
  • 使用lighthouse測試頁面性能,根據評估結果,針對性優化。
  • 錯誤監控 / 數據統計

  • 通常監聽load事件註冊
  • 做用域
    //只會對topics/下面的路徑進行優化
    navigator.serviceWorker.register('/topics/sw.js');
    複製代碼

  • 生命週期javascript

    • 註冊

      指定serviceworkerJS文件的位置,加載解析執行;load事件中註冊css

    • 安裝

      將指定的靜態資源進行離線緩存html

    • 激活

      對舊緩存作刪除等處理;接管控制權前端

  • SW更新機制
    java

    背景SW 沒有自動更新的邏輯,它須要在頁面加載(一次跳轉)以後纔會去請求sw.js
    解決:因爲瀏覽器判斷sw.js是否更新是經過字節方式,所以修改cacheName會從新觸發install並緩存資源。此外,在activate事件中,咱們須要檢查cacheName是否變化,若是變化則表示有了新的緩存資源,原有緩存須要刪除。node

    • 新的SW.js文件下載,並觸發install事件。
    • 此時,舊的SW還在工做,新的SW進入waiting狀態(此時兩個SW同時存在,舊的SW掌管當前頁面)。
      • 等到下一次頁面跳轉(二次跳轉)才能展現最新的頁面。
      • install中緩存資源後,self.skipWaiting():
        //caches是全局變量
        self.addEventListener('install',e =>{
          e.waitUntil(
            caches.open(cacheStorageKey)
            .then(cache => cache.addAll(cacheList))
            .then(() => self.skipWaiting())
          )
        })
        複製代碼
      • 【workbox另外一種思路】由於新的SW會進入waiting狀態,因此在waiting階段,採用必定的策略來進行頁面的刷新:如彈窗提示用戶是否刷新,若刷新則調用wb.messageSW({type: 'SKIP_WAITING'});觸發message事件。【webWorker】main.js中作相關邏輯處理,經過postMessage傳遞消息控制worker
        self.addEventListener('message', (event) => {
            if (event.data && event.data.type === 'SKIP_WAITING') {
                // the new v2 Service Worker will immediately kill the old v1 activated Service     Worker once the v2 Service Worker installs.
                self.skipWaiting();
            }
        });
        複製代碼
    • 一旦新的SW接管,則會觸發activate事件;能夠在此處對舊緩存作刪除等處理;不重刷,接管控制權:self.clients.claim()
      self.addEventListener('activate',function(e){
          e.waitUntil(
              //獲取全部cache名稱
              caches.keys().then(cacheNames => {
              return Promise.all(
                  // 獲取全部不一樣於當前版本名稱cache下的內容
                  cacheNames.filter(cacheNames => {
                      return cacheNames !== cacheStorageKey
                  }).map(cacheNames => {
                      return caches.delete(cacheNames)
                  })
              )
              }).then(() => {
                  //直接接管當前頁面的權限
                  return self.clients.claim()
              })
          )
      })
      複製代碼
  • 注意react

    • 設置sw.jsmanifest.json靜態資源的緩存策略:不緩存,必須校驗
    • 冷啓動,預加載
    • 只緩存重要的頁面如主頁,連接,最近的文章等
    • 不緩存圖片,視頻和大的文件
    • 按期清除舊的緩存文件
    • 提供一個」緩存到本地」的按鈕,以便用戶能夠自行選擇
    • 降級方案:增長降級開關
      if('serviceWorker' in navigator) {
          fetch('./cas').then(() => {
              if(降級) {
                  //註銷掉全部sw
                  unregister();
              }else {
                  //註冊
                  register();
              }
          })
      }
      複製代碼
  • 首次啓動優化
    背景:首次加載時沒有資源,因此會走線上,等於沒優化
    方案:構建時,把整個項目用到的資源輸出到一個list,而後inline到sw.js。當sw install,就會把這個list的資源所有請求進行緩存。這樣作的結果是,不管用戶第一次進入咱們站點的哪一個頁面,都會把整個站點全部的資源都加載回來並緩存。 webpack

Redux 與 IndexDB 結合


2、資源預加載(靜態 & 動態)

涉及內容:
 - link相關(rel、media)
 - defer、async
 - 緩存(4種緩存、緩存策略、ServiceWork)
 - 優化網絡(H2 PushPreload/Prefetch、域名拆分)
 - 推送JSON/json內聯,加速首頁渲染
 - 瀏覽器中各資源加載的優先級git

前言

  • 背景

當咱們須要某些網絡資源時,加載和執行每每耦合在一塊兒,下載完當即執行,而加載過程是阻塞式的,延長了onload時間。所以如何在資源執行前預加載資源,減小等待網絡的開銷即是咱們要探討的問題。github

  • 常規作法

    附一張不一樣資源瀏覽器優先級的圖示(來源):

    不一樣資源瀏覽器優先級

    1. async/defer: 無阻塞加載

      async defer

      • defer:DOMContentLoaded事件觸發前執行
        在現實當中,延遲腳本並不必定會按照順序執行,也不必定會在DOMContentLoaded事件觸發前執行,所以最好只包含一個延遲腳本
      • async:加載完當即執行,沒法控制執行時機和執行順序。適用於無依賴的外部獨立資源

      不足:僅限於腳本資源;執行時機不可控或存在執行順序問題,用於非關鍵資源。

    2. 使用ajax加載資源:能夠實現預加載。

      不足:優先級較低,沒法對首屏資源提早加載。

    3. Webkit瀏覽器預測解析:chrome的預加載掃描器html-preload-scanner經過掃描節點中的 "src" , "link"等屬性,找到外部鏈接資源後進行預加載,避免了資源加載的等待時間,一樣實現了提早加載以及加載和執行分離。
      原始解析作法:


      採用預解析(掃描)器:
      不足:

      • 僅限解析HTML中收集到的外鏈資源,對JS異步加載的資源沒法提早收集。
      • 未暴露相似於Preload的onload事件。
    4. Server Push  圖片來源

      Link: <https://example.com/other/styles.css>; rel=preload; as=style;
      複製代碼

      僅預加載,不推送:

      Link: <https://example.com/other/styles.css>; rel=preload; as=style;nopush
      複製代碼

      目標:減小請求數量和提升頁面加載速度。
      特色:多頁面共享push cache(動態數據json除外
      適用場景:若是不推送這個資源,瀏覽器就會請求這個資源。
      須要注意:要確保沒有發起沒必要要的推送,浪費流量。可使用preload標籤代替,或者在HTTP頭中加nopush屬性。
      【若是服務器或者瀏覽器不支持 HTTP/2,那麼瀏覽器就會按照 preload 來處理這個頭信息,預加載指定的資源文件。】

      不足:

      • Edge和Safari的支持很差,慎用
      • 若是瀏覽器不從push cache中獲取資源,推送反而不利於網頁性能。
      • 只能推送不帶請求體的GET和HEAD請求
      • push cache中的資源只能使用一次
      • 不考慮客戶端的緩存,始終推送。
        • 只對第一次訪問的用戶開啓服務器推送;
        • 保守起見,推送本來內聯的資源,這樣即便多推,也比內聯效果好點。
        • 將資源文件的緩存狀態更新至客戶端的Cookie
          • cookie空間有限,可使用 Golomb-compressed sets算法生成指紋,編碼爲base64,而後存入Cookie
          • 需自行處理緩存策略
      • 僅能推送同源資源
      • push cache: 只在會話中存在,一旦會話結束就會被釋放
      • 即便push的是最新的資源,若是http緩存中max-age沒有過時,仍然使用http緩存中的資源。(【擴展】資源依次查找緩存的順序:內存緩存、Service Worker緩存、Disk緩存、Push緩存
      • 無load/error事件

【擴展】

 問題1:不只js渲染阻塞,同時js執行後可能獲取一些數據(JSON),才能真正渲染完成,如何解決?

  1. 用於動態資源的提早推送,注意參數需固定,不帶隨機變量的
  2. 服務端渲染(直出同構)
  3. 內聯JSON(目前有些工程使用此方法傳遞同步數據)
    另外兩種較常見的渲染方式圖片來源:

preload和prefetch

  • 概念

    • preload:聲明式的 fetch,能夠強制瀏覽器請求資源,同時不阻塞文檔 onload 事件。當前頁面使用,儘早下載,優先級較高;
    • prefetch:首次渲染時不須要,以後可能須要。優先級較低,在瀏覽器空閒時纔會下載。使用場景:好比當前頁可能跳轉的頁面,或者條件加載的資源。
  • 特色

    • preload的資源存儲在內存緩存(沒有設置資源的緩存策略時)中。
    • 下載但不執行
    • 異步加載,不影響當前頁面的渲染
    • 提早加載資源,在真正使用時,直接從從緩存中讀取。
    • 使用場景
      • 當分析當前頁面用戶高頻點擊的連接,分析提取跳轉頁上的資源,使用prefetch預加載。
      • font字體文件的預加載
        因爲字體文件必須等到 CSSOM 構建完成而且做用到頁面元素了纔會開始加載,會致使頁面字體樣式閃動。而瀏覽器爲了不FOUT,會盡可能等待字體加載完成後,再顯示應用了該字體的內容,會致使加載完成前顯示空白。
  • 檢測prelaod和prefetch的支持狀況

    let { relList } = document.createElement('link');
    return relList && relList.supports && relList.supports('preload');
    複製代碼
  • 如何使用

    //link標籤
    <link rel="preload"  as="style" herf="./a.css"/>
    <link rel="prefetch" as="script" href="./b.js"/>
    
    //動態建立
    let preLink = document.createElement('link');
    preLink.rel ='prefetch';  //感受動態建立不適合preload
    prelink.as = 'script';
    preLink.href = './a.js';
    複製代碼
  • as屬性值

    不一樣值代表資源類型,對應的優先級不一樣:style, script, image, media, document, font。 問題: 官方說法:不帶 「as」屬性的 preload 的優先級將會等同於異步請求。 測試:沒有發請求。

  • 注意事項

    • 形成二次下載
    • 沒有使用preload資源,Chrome會在onload事件3s後作出警示,避免無效的優化,浪費流量。
    • 【擴展】瀏覽器對同一域名有並行加載數限制,所以考慮域名拆分等優化。
  • 實踐

    twitter
    //head中
    <link rel="preload" href="https://abs.twimg.com/k/zh-cn/init.zh-cn.3b38ddbf651139df6007.js" as="script">
    //body底部
    <script src="https://abs.twimg.com/k/zh-cn/init.zh-cn.3b38ddbf651139df6007.js" async></script>
    
    複製代碼
  • preload的polyfill

    • 背景知識
    //若支持preload,異步下載完不會當即執行
    <link rel="preload" >
    //下載完當即應用到DOM樹
    <link rel="stylesheet" >
    //異步下載,只有打印的時候纔會應用,不符合則不會應用,所以不會阻塞渲染
    <link rel="stylesheet" media='print' >
    
    複製代碼
    • polyfill思路 (參見loadCSS,提供了css的preload的polyfill實現)
    // 1. 支持preload:
        //因爲preload只是獲取樣式,不會當即應用,所以使用onload改變link的rel使其當即生效9)
        <link rel="preload" href="style.css" as="style" onload="this.onload = null;this.rel ='stylesheet'">
        注:設置onload=null主要是由於有些瀏覽器會在rel改變時再次出發load事件。
    // 2. 不支持preload
        // 1)獲取所有link
            let links = document.getElementsByTagName("link");
        
        // 2)緩存每一個link的media
            var finalMedia = link.media || "all";
        
        // 3)改變link的rel和media(異步下載但不會應用)
            link.rel = "stylesheet";
            link.media = "only x";
    	
        // 4)若是綁定onload事件(爲了啓用media)
            if( link.addEventListener ){
    	    	link.addEventListener( "load", enableStylesheet );
    	    } else if( link.attachEvent ){
    	    	link.attachEvent( "onload", enableStylesheet );
        	}
        // 5)爲了應對舊的瀏覽器不支持link的onload事件
            setTimeout( enableStylesheet, 3000 );
    	
        // 6)enableStylesheet回調,將media恢復,樣式當即應用
            link.setAttribute( "onload", null ); 
            link.media = finalMedia;
    複製代碼

    適用於對於首頁無關的樣式:因爲preload的資源,可以異步加載樣式,所以能夠避免在加載首頁無關樣式時阻塞初始渲染。

    對於首頁初始渲染中重要的樣式
      1)內聯 (注意,會將靜態資源的緩存策略與頁面的緩存策略捆綁
      2)HTTP/2的serverPush

知道了preload和prefetch的用途,那如何結合項目實踐呢?因爲webpack目前基本是項目必備,因此首先介紹結合webpack的使用;而後對quiklink進行簡單介紹。

結合webpack的實踐

1. 插件:PreloadWebpackPlugin

經常使用的配置以下:

new PreloadWebpackPlugin({
   //preload or  prefetch方式
   rel: '',
   /*
    *即<link as='' />中的as,代表資源類型,不一樣的類型決定了不一樣的執行優先級
    *好比:script的優先級大於style
   */
   as: '',
  //排除的html頁面集合,即只關聯要配置的頁面
   excludeHtmlNames: [],
   //所關聯頁面須要使用preload或prefetch的資源
   include: []
})
複製代碼

其中include的兩種使用:

  1. 根據chunk類型進行處理:
    • asyncChunks:import()動態導入的模塊。可使用prefetch方式異步加載模塊;
    • initial:初始化須要的模塊;
    • allChunks:處理全部模塊(asyncChunks & initial)。
  2. 對已知命名的chunk,能夠更精確的使用數組的方式配置須要使用的chunk
```
  include: ['vendor', 'index']
 ```
複製代碼
  • 注意事項

    1. 須要結合HtmlWebpackPlugin插件使用
    2. 必須放到HtmlWebpackPlugin後面,由於PreloadWebpackPlugin須要使用其提供的hook鉤子將構造的<link>插入html中:
    plugins: [
      new HtmlWebpackPlugin(),
      new PreloadWebpackPlugin()
    ]
    複製代碼
  • 使用效果 對某個頁面中include的資源,最終會在對應頁面head中插入link標籤:

    <link as="script" href="/common.js" rel="preload">
    <link as="script" href="/asyncChunk.js" rel="prefetch">
    複製代碼

    當真正使用時,因爲已經下載到本地,直接讀取執行,性能獲得較大的提高。

2. 結合import()

好處:拆分chunk,減小首屏js體積。
若是工程沒有使用HtmlWebpackPlugin,能夠對動態導入的資源作以下處理:

import(/* webpackPrefetch: true */)
import(/* webpackPreload: true */)
複製代碼

【版本限制】需webpack v4.6.0+ 才支持預取和預加載。本地測試後,發現prefetch可用,preload無效(有成功的煩請告知)。


quiklink

  • 工做原理 經過獲取頁面中a標籤的href,試圖更快的加載接下來可能要訪問的頁面。

    • IntersectionObserver(交叉觀察器): 檢測當前視口的links

      let target= document.getElementById('a');
      io = new IntersectionObserver(
          entries => {}, 
          {
            threshold: [1]  //交叉區域爲1時會觸發callback
          }
      );
      io.observe(target);
      複製代碼

      【備註】常規的主要是經過getBoundingClientRect()獲取元素在視口中的詳細位置,來實現滾動加載以及吸附等功能。

    • requestIdleCallback:等到瀏覽器空閒時

      【備註】注意其和requestAnimationFrame的區別

    • 檢查當前的網絡環境:navigator.connection.effectiveType //4G、2G...

    • prefetch緩存的待下載的url

    小巧的js庫,使用瞭如上4個特性,每個都值得細細品味。

  • 工做流程

    • 瀏覽器空閒時,獲取頁面全部a標籤的連接link
      • 使用IntersectionObserver監聽link
        • 在視口區的link,使用prefetch下載
          • 判斷當前網絡情況,若使用的是2G或者開啓了省流模式(data-saver),則不作處理

            data-saver: The user may enable such preference, if made available by the user agent, due to high data transfer costs, slow connection speeds, or other reasons.

            題外話:prefetch有點偷流量的意思,我想看什麼才消耗對應資源產生的流量,而prefetch擅自爲我作主,偷偷下載不少我可能並不須要的資源(在早前流量特貴的時候這麼作,估計會被打死...)。

          • 三種下載資源的方式:fetchxhr<link rel=prefetch href="" />

  • 使用說明

    • 只支持prefetch
    • a標籤獲取links
    • 最佳實踐是對後續可能訪問的頁面的提早下載,後續真正訪問時,直接從本地獲取執行

總結

綜合來看,PreloadWebpackPlugin更適合對chunk而非html文件的處理;而quikLink更適合博客類的網站,或者服務端渲染的頁面,這樣才能實現"秒開"的預期效果。

參考文獻

【歡迎留言】本文是否對你有幫助,亦或有所遺漏筆誤等,煩請告知。

相關文章
相關標籤/搜索