一篇文章搞定前端性能優化面試

前言

雖然前端開發做爲 GUI 開發的一種,可是存在其特殊性,前端的特殊性就在於「動態」二字,傳統 GUI 開發,無論是桌面應用仍是移動端應用都是須要預先下載的,只有先下載應用程序纔會在本地操做系統運行,而前端不一樣,它是「動態增量」式的,咱們的前端應用每每是實時加載執行的,並不須要預先下載,這就形成了一個問題,前端開發中每每最影響性能的不是什麼計算或者渲染,而是加載速度,加載速度會直接影響用戶體驗和網站留存。javascript

《Designing for Performance》的做者 Lara Swanson在2014年寫過一篇文章《Web性能即用戶體驗》,她在文中提到「網站頁面的快速加載,可以創建用戶對網站的信任,增長回訪率,大部分的用戶其實都期待頁面可以在2秒內加載完成,而當超過3秒之後,就會有接近40%的用戶離開你的網站」。css

值得一提的是,GUI 開發依然有一個共同的特殊之處,那就是 體驗性能 ,體驗性能並不指在絕對性能上的性能優化,而是迴歸用戶體驗這個根本目的,由於在 GUI 開發的領域,絕大多數狀況下追求絕對意義上的性能是沒有意義的.html

好比一個動畫原本就已經有 60 幀了,你經過一個吊炸天的算法優化到了 120 幀,這對於你的 KPI 毫無用處,由於這個優化自己沒有意義,由於除了少數特異功能的異人,沒有人能分得清 60 幀和 120 幀的區別,這對於用戶的體驗沒有任何提高,相反,一個首屏加載須要 4s 的網站,你沒有任何實質意義上的性能優化,只是加了一個設計姐姐設計的 loading 圖,那這也是十分有意義的優化,由於好的 loading 能夠減小用戶焦慮,讓用戶感受沒有等過久,這就是用戶體驗級的性能優化.前端

所以,咱們要強調即便沒有對性能有實質的優化,經過設計提升用戶體驗的這個過程,也算是性能優化,由於 GUI 開發直面用戶,你讓用戶有了性能快的 錯覺,這也叫性能優化了,畢竟用戶以爲快,纔是真的快...vue

文章目錄

  1. 首屏加載優化
  2. 路由跳轉加載優化

1.首屏加載

首屏加載是被討論最多的話題,一方面web 前端首屏的加載性能的確廣泛較差,另外一方面,首屏的加載速度相當重要,不少時候過長的白屏會致使用戶尚未體驗到網站功能的時候就流失了,首屏速度是用戶留存的關鍵點。java

以用戶體驗的角度來解讀首屏的關鍵點,若是做爲用戶咱們從輸入網址以後的內心過程是怎樣的呢?node

當咱們敲下回車後,咱們第一個疑問是:
"它在運行嗎?" 
這個疑問一直到用戶看到頁面第一個繪製的元素爲止,這個時候用戶才能肯定本身的請求是有效的(而不是被牆了...),而後第二個疑問:
"它有用嗎?" 
若是隻繪製出無心義的各類亂序的元素,這對於用戶是不可理解的,此時雖然頁面開始加載了,可是對於用戶沒有任何價值,直到文字內容、交互按鈕這些元素加載完畢,用戶才能理解頁面,這個時候用戶會嘗試與頁面交互,會有第三個疑問:
"它能使用了嗎?" 
直到用戶成功與頁面互動,這纔算是首屏加載完畢了.react

在第一個疑問和第二個疑問之間的等待期,會出現白屏,這是優化的關鍵.webpack

1.1 白屏的定義

無論是咱們如何優化性能,首屏必然是會出現白屏的,由於這是前端開發這項技術的特色決定的。git

那麼咱們先定義一下白屏,這樣才能方便計算咱們的白屏時間,由於白屏的計算邏輯說法不一,有人說要從首次繪製(First Paint,FP)算起到首次內容繪製(First Contentful Paint,FCP)這段時間算白屏,我我的是不一樣意的,我我的更傾向因而從路由改變起(即用戶再按下回車的瞬間)到首次內容繪製(即能看到第一個內容)爲止算白屏時間,由於按照用戶的心理,在按下回車起就認爲本身發起了請求,而直到看到第一個元素被繪製出來以前,用戶的內心是焦慮的,由於他不知道這個請求會不會被響應(網站掛了?),不知道要等多久纔會被響應到(網站慢?),這期間爲用戶首次等待期間。

白屏時間 = firstPaint - performance.timing.navigationStart

以webapp 版的微博爲例(微博爲數很少的的良心產品),通過 Lighthouse(谷歌的網站測試工具)它的白屏加載時間爲 2s,是很是好的成績。
image.png


1.2 白屏加載的問題分析

在現代前端應用開發中,咱們每每會用 webpack 等打包器進行打包,不少狀況下若是咱們不進行優化,就會出現不少體積巨大的 chunk,有的甚至在 5M 左右(我第一次用 webpack1.x 打包的時候打出了 8M 的包),這些 chunk 是加載速度的殺手。

瀏覽器一般都有併發請求的限制,以 Chrome 爲例,它的併發請求就爲 6 個,這致使咱們必須在請求完前 6 個以後,才能繼續進行後續請求,這也影響咱們資源的加載速度。

固然了,網絡、帶寬這是自始至終都影響加載速度的因素,白屏也不例外.

1.3 白屏的性能優化

咱們先梳理下白屏時間內發生了什麼:

  1. 回車按下,瀏覽器解析網址,進行 DNS 查詢,查詢返回 IP,經過 IP 發出 HTTP(S) 請求
  2. 服務器返回HTML,瀏覽器開始解析 HTML,此時觸發請求 js 和 css 資源
  3. js 被加載,開始執行 js,調用各類函數建立 DOM 並渲染到根節點,直到第一個可見元素產生
1.3.1 loading 提示

若是你用的是以 webpack 爲基礎的前端框架工程體系,那麼你的index.html 文件必定是這樣的:

<div id="root"></div>

咱們將打包好的整個代碼都渲染到這個 root 根節點上,而咱們如何渲染呢?固然是用 JavaScript 操做各類 dom 渲染,好比 react 確定是調用各類 _React_._createElement_(),這是很耗時的,在此期間雖然 html 被加載了,可是依然是白屏,這就存在操做空間,咱們能不能在 js 執行期間先加入提示,增長用戶體驗呢?

是的,咱們通常有一款 webpack 插件叫html-webpack-plugin ,在其中配置 html 就能夠在文件中插入 loading 圖。

webpack 配置:

const HtmlWebpackPlugin = require('html-webpack-plugin')
const loading = require('./render-loading') // 事先設計好的 loading 圖

module.exports = {
  entry: './src/index.js',
  output: {
    path: __dirname + '/dist',
    filename: 'index_bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './src/index.html',
      loading: loading
    })
  ]
}
1.3.2 (僞)服務端渲染

那麼既然在 HTML 加載到 js 執行期間會有時間等待,那麼爲何不直接服務端渲染呢?直接返回的 HTML 就是帶完整 DOM 結構的,免得還得調用 js 執行各類建立 dom 的工做,不只如此還對 SEO 友好。

正是有這種需求 vue 和 react 都支持服務端渲染,而相關的框架Nuxt.js、Next.js也大行其道,固然對於已經採用客戶端渲染的應用這個成本過高了。

因而有人想到了辦法,谷歌開源了一個庫Puppeteer,這個庫實際上是一個無頭瀏覽器,經過這個無頭瀏覽器咱們能用代碼模擬各類瀏覽器的操做,好比咱們就能夠用 node 將 html 保存爲 pdf,能夠在後端進行模擬點擊、提交表單等操做,天然也能夠模擬瀏覽器獲取首屏的 HTML 結構。

prerender-spa-plugin就是基於以上原理的插件,此插件在本地模擬瀏覽器環境,預先執行咱們的打包文件,這樣經過解析就能夠獲取首屏的 HTML,在正常環境中,咱們就能夠返回預先解析好的 HTML 了。

1.3.3 開啓 HTTP2

咱們看到在獲取 html 以後咱們須要自上而下解析,在解析到 script 相關標籤的時候才能請求相關資源,並且因爲瀏覽器併發限制,咱們最多一次性請求 6 次,那麼有沒有辦法破解這些困境呢?

http2 是很是好的解決辦法,http2 自己的機制就足夠快:

  1. http2採用二進制分幀的方式進行通訊,而 http1.x 是用文本,http2 的效率更高
  2. http2 能夠進行多路複用,即跟同一個域名通訊,僅須要一個 TCP 創建請求通道,請求與響應能夠同時基於此通道進行雙向通訊,而 http1.x 每次請求須要創建 TCP,屢次請求須要屢次鏈接,還有併發限制,十分耗時

image.png

  1. http2 能夠頭部壓縮,可以節省消息頭佔用的網絡的流量,而HTTP/1.x每次請求,都會攜帶大量冗餘頭信息,浪費了不少帶寬資源
例如:下圖中的兩個請求, 請求一發送了全部的頭部字段,第二個請求則只須要發送差別數據,這樣能夠減小冗餘數據,下降開銷

image.png

  1. http2能夠進行服務端推送,咱們平時解析 HTML 後碰到相關標籤纔會進而請求 css 和 js 資源,而 http2 能夠直接將相關資源直接推送,無需請求,這大大減小了屢次請求的耗時

咱們能夠點擊此網站 進行 http2 的測試

我曾經作個一個測試,http2 在網絡通暢+高性能設備下的表現沒有比 http1.1有明顯的優點,可是網絡越差,設備越差的狀況下 http2 對加載的影響是質的,能夠說 http2 是爲移動 web 而生的,反而在光纖加持的高性能PC 上優點不太明顯.
1.3.4 開啓瀏覽器緩存

既然 http 請求如此麻煩,能不能咱們避免 http 請求或者下降 http 請求的負載來實現性能優化呢?

利用瀏覽器緩存是很好的辦法,他能最大程度上減小 http 請求,在此以前咱們要先回顧一下 http 緩存的相關知識.

咱們先羅列一下和緩存相關的請求響應頭。

  • Expires
響應頭,表明該資源的過時時間。
  • Cache-Control
請求/響應頭,緩存控制字段,精確控制緩存策略。
  • If-Modified-Since
請求頭,資源最近修改時間,由瀏覽器告訴服務器。
  • Last-Modified
響應頭,資源最近修改時間,由服務器告訴瀏覽器。
  • Etag
響應頭,資源標識,由服務器告訴瀏覽器。
  • If-None-Match
請求頭,緩存資源標識,由瀏覽器告訴服務器。

配對使用的字段:

  • If-Modified-Since 和 Last-Modified
  • Etag 和 If-None-Match

當無本地緩存的時候是這樣的:

當有本地緩存但沒過時的時候是這樣的:
image.png
當緩存過時了會進行協商緩存:
image.png
瞭解到了瀏覽器的基本緩存機制咱們就好進行優化了.

一般狀況下咱們的 WebApp 是有咱們的自身代碼和第三方庫組成的,咱們自身的代碼是會經常變更的,而第三方庫除非有較大的版本升級,否則是不會變的,因此第三方庫和咱們的代碼須要分開打包,咱們能夠給第三方庫設置一個較長的強緩存時間,這樣就不會頻繁請求第三方庫的代碼了。

那麼如何提取第三方庫呢?在 webpack4.x 中, SplitChunksPlugin 插件取代了 CommonsChunkPlugin 插件來進行公共模塊抽取,咱們能夠對SplitChunksPlugin 進行配置進行 拆包 操做。

SplitChunksPlugin配置示意以下:
optimization: {
    splitChunks: { 
      chunks: "initial",         // 代碼塊類型 必須三選一: "initial"(初始化) | "all"(默認就是all) | "async"(動態加載) 
      minSize: 0,                // 最小尺寸,默認0
      minChunks: 1,              // 最小 chunk ,默認1
      maxAsyncRequests: 1,       // 最大異步請求數, 默認1
      maxInitialRequests: 1,     // 最大初始化請求書,默認1
      name: () => {},            // 名稱,此選項課接收 function
      cacheGroups: {                // 緩存組會繼承splitChunks的配置,可是test、priorty和reuseExistingChunk只能用於配置緩存組。
        priority: "0",              // 緩存組優先級,即權重 false | object |
        vendor: {                   // key 爲entry中定義的 入口名稱
          chunks: "initial",        // 必須三選一: "initial"(初始化) | "all" | "async"(默認就是異步)
          test: /react|lodash/,     // 正則規則驗證,若是符合就提取 chunk
          name: "vendor",           // 要緩存的 分隔出來的 chunk 名稱
          minSize: 0,
          minChunks: 1,
          enforce: true,
          reuseExistingChunk: true   // 可設置是否重用已用chunk 再也不建立新的chunk
        }
      }
    }
  }

SplitChunksPlugin 的配置項不少,能夠先去官網瞭解如何配置,咱們如今只簡單列舉了一下配置元素。

若是咱們想抽取第三方庫能夠這樣簡單配置

splitChunks: {
      chunks: 'all',   // initial、async和all
      minSize: 30000,   // 造成一個新代碼塊最小的體積
      maxAsyncRequests: 5,   // 按需加載時候最大的並行請求數
      maxInitialRequests: 3,   // 最大初始化請求數
      automaticNameDelimiter: '~',   // 打包分割符
      name: true,
      cacheGroups: {
        vendor: {
          name: "vendor",
          test: /[\\/]node_modules[\\/]/, //打包第三方庫
          chunks: "all",
          priority: 10 // 優先級
        },
        common: { // 打包其他的的公共代碼
          minChunks: 2, // 引入兩次及以上被打包
          name: 'common', // 分離包的名字
          chunks: 'all',
          priority: 5
        },
      }
    },

這樣彷佛大功告成了?並無,咱們的配置有很大的問題:

  1. 咱們粗暴得將第三方庫一塊兒打包可行嗎? 固然是有問題的,由於將第三方庫一塊打包,只要有一個庫咱們升級或者引入一個新庫,這個 chunk 就會變更,那麼這個chunk 的變更性會很高,並不適合長期緩存,還有一點,咱們要提升首頁加載速度,第一要務是減小首頁加載依賴的代碼量,請問像 react vue reudx 這種整個應用的基礎庫咱們是首頁必需要依賴的以外,像 d3.js three.js這種特定頁面纔會出現的特殊庫是不必在首屏加載的,因此咱們須要將應用基礎庫和特定依賴的庫進行分離。
  2. 當 chunk 在強緩存期,可是服務器代碼已經變更了咱們怎麼通知客戶端?上面咱們的示意圖已經看到了,當命中的資源在緩存期內,瀏覽器是直接讀取緩存而不會向服務器確認的,若是這個時候服務器代碼已經變更了,怎麼辦?這個時候咱們不能將 index.html 緩存(反正webpack時代的 html 頁面小到沒有緩存的必要),須要每次引入 script 腳本的時候去服務器更新,並開啓 hashchunk,它的做用是當 chunk 發生改變的時候會生成新的 hash 值,若是不變就不發生變更,這樣當 index 加載後續 script資源時若是 hashchunk 沒變就會命中緩存,若是改變了那麼會從新去服務端加載新資源。
下圖示意瞭如何將第三方庫進行拆包,基礎型的 react 等庫與工具性的 lodash 和特定庫 Echarts 進行拆分
cacheGroups: {
        reactBase: {
          name: 'reactBase',
          test: (module) => {
              return /react|redux/.test(module.context);
          },
          chunks: 'initial',
          priority: 10,
        },
        utilBase: {
          name: 'utilBase',
          test: (module) => {
              return /rxjs|lodash/.test(module.context);
          },
          chunks: 'initial',
          priority: 9,
        },
        uiBase: {
          name: 'chartBase',
          test: (module) => {
              return /echarts/.test(module.context);
          },
          chunks: 'initial',
          priority: 8,
        },
        commons: {
          name: 'common',
          chunks: 'initial',
          priority: 2,
          minChunks: 2,
        },
      }
咱們對 chunk 進行 hash 化,正以下圖所示,咱們變更 chunk2 相關的代碼後,其它 chunk 都沒有變化,只有 chunk2 的 hash 改變了
output: {
    filename: mode === 'production' ? '[name].[chunkhash:8].js' : '[name].js',
    chunkFilename: mode === 'production' ? '[id].[chunkhash:8].chunk.js' : '[id].js',
    path: getPath(config.outputPath)
  }

image.pngimage.png

咱們經過 http 緩存+webpack hash 緩存策略使得前端項目充分利用了緩存的優點,可是 webpack 之因此須要傳說中的 webpack配置工程師 是有緣由的,由於 webpack 自己是玄學,仍是以上圖爲例,若是你 chunk2的相關代碼去除了一個依賴或者引入了新的可是已經存在工程中依賴,會怎麼樣呢?

咱們正常的指望是,只有 chunk2 發生變化了,可是事實上是大量不相干的 chunk 的 hash 發生了變更,這就致使咱們緩存策略失效了,下圖是變動後的 hash,咱們用紅圈圈起來的都是 hash 變更的,而事實上咱們只變更了 chunk2 相關的代碼,爲何會這樣呢?

image.png
緣由是 webpack 會給每一個 chunk 搭上 id,這個 id 是自增的,好比 chunk 0 中的id 爲 0,一旦咱們引入新的依賴,chunk 的自增會被打亂,這個時候又由於 hashchunk 根據內容生成 hash,這就致使了 id 的變更導致 hashchunk 發生鉅變,雖然代碼內容根本沒有變化。image.png
這個問題咱們須要額外引入一個插件HashedModuleIdsPlugin,他用非自增的方式進行 chunk id 的命名,能夠解決這個問題,雖然 webpack 號稱 0 配置了,可是這個經常使用功能沒有內置,要等到下個版本了。
image.png

webpack hash緩存相關內容建議閱讀此 文章 做爲拓展

1.4 FMP(首次有意義繪製)

在白屏結束以後,頁面開始渲染,可是此時的頁面還只是出現個別無心義的元素,好比下拉菜單按鈕、或者亂序的元素、導航等等,這些元素雖然是頁面的組成部分可是沒有意義.
什麼是有意義?
對於搜索引擎用戶是完整搜索結果
對於微博用戶是時間線上的微博內容
對於淘寶用戶是商品頁面的展現

那麼在FCP 和 FMP 之間雖然開始繪製頁面,可是整個頁面是沒有意義的,用戶依然在焦慮等待,並且這個時候可能出現亂序的元素或者閃爍的元素,很影響體驗,此時咱們可能須要進行用戶體驗上的一些優化。
Skeleton是一個好方法,Skeleton如今已經很開始被普遍應用了,它的意義在於事先撐開即將渲染的元素,避免閃屏,同時提示用戶這要渲染東西了,較少用戶焦慮。

好比微博的Skeleton就作的很不錯

image.png
在不一樣框架上都有相應的Skeleton實現
React: antd 內置的骨架圖Skeleton方案
Vue: vue-skeleton-webpack-plugin 

以 vue-cli 3 爲例,咱們能夠直接在vue.config.js 中配置
//引入插件
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');

module.exports = {
    // 額外配置參考官方文檔
    configureWebpack: (config)=>{
        config.plugins.push(new SkeletonWebpackPlugin({
            webpackConfig: {
                entry: {
                    app: path.join(__dirname, './src/Skeleton.js'),
                },
            },
            minimize: true,
            quiet: true,
        }))
    },
    //這個是讓骨架屏的css分離,直接做爲內聯style處理到html裏,提升載入速度
    css: {
        extract: true,
        sourceMap: false,
        modules: false
    }
}

而後就是基本的 vue 文件編寫了,直接看文檔便可。

1.5 TTI(可交互時間)

當有意義的內容渲染出來以後,用戶會嘗試與頁面交互,這個時候頁面並非加載完畢了,而是看起來頁面加載完畢了,事實上這個時候 JavaScript 腳本依然在密集得執行.

咱們看到在頁面已經基本呈現的狀況下,依然有大量的腳本在執行

image.png
這個時候頁面並非可交互的,直到TTI 的到來,TTI 到來以後用戶就能夠跟頁面進行正常交互的,TTI 通常沒有特別精確的測量方法,廣泛認爲知足FMP && DOMContentLoader事件觸發 && 頁面視覺加載85%這幾個條件後,TTI 就算是到來了。

在頁面基本呈現到能夠交互這段時間,絕大部分的性能消耗都在 JavaScript 的解釋和執行上,這個時候決定 JavaScript 解析速度的無非一下兩點:

  1. JavaScript 腳本體積
  2. JavaScript 自己執行速度

JavaScript 的體積問題咱們上一節交代過了一些,咱們能夠用SplitChunksPlugin拆庫的方法減少體積,除此以外還有一些方法,咱們下文會交代。

1.5.1 Tree Shaking

Tree Shaking雖然出現很早了,好比js基礎庫的事實標準打包工具 rollup 就是Tree Shaking的祖師爺,react用 rollup 打包以後體積減小了 30%,這就是Tree Shaking的厲害之處。

Tree Shaking的做用就是,經過程序流分析找出你代碼中無用的代碼並剔除,若是不用Tree Shaking那麼不少代碼雖然定義了可是永遠都不會用到,也會進入用戶的客戶端執行,這無疑是性能的殺手,Tree Shaking依賴es6的module模塊的靜態特性,經過分析剔除無用代碼.

目前在 webpack4.x 版本以後在生產環境下已經默認支持Tree Shaking了,因此Tree Shaking能夠稱得上開箱即用的技術了,可是並不表明Tree Shaking真的會起做用,由於這裏面仍是有不少坑.

坑 1: Babel 轉譯,咱們已經提到用Tree Shaking的時候必須用 es6 的module,若是用 common.js那種動態module,Tree Shaking就失效了,可是 Babel 默認狀態下是啓用 common.js的,因此須要咱們手動關閉.
坑 2: 第三方庫不可控,咱們已經知道Tree Shaking的程序分析依賴 ESM,可是市面上不少庫爲了兼容性依然只暴露出了ES5 版本的代碼,這致使Tree Shaking對不少第三方庫是無效的,因此咱們要儘可能依賴有 ESM 的庫,好比以前有一個 ESM 版的 lodash(lodash-es),咱們就能夠這樣引用了import { dobounce } from 'lodash-es'

1.5.2 polyfill動態加載

polyfill是爲了瀏覽器兼容性而生,是否須要 polyfill 應該有客戶端的瀏覽器本身決定,而不是開發者決定,可是咱們在很長一段時間裏都是開發者將各類 polyfill 打包,其實不少狀況下致使用戶加載了根本沒有必要的代碼.

解決這個問題的方法很簡單,直接引入 <script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script> 便可,而對於 Vue 開發者就更友好了,vue-cli 如今生成的模板就自帶這個引用.
image.png

這個原理就是服務商經過識別不一樣瀏覽器的瀏覽器User Agent,使得服務器可以識別客戶使用的操做系統及版本、CPU 類型、瀏覽器及版本、瀏覽器渲染引擎、瀏覽器語言、瀏覽器插件等,而後根據這個信息判斷是否須要加載 polyfill,開發者在瀏覽器的 network 就能夠查看User Agent。
image.png

1.5.3 動態加載 ES6 代碼

既然 polyfill 能動態加載,那麼 es5 和 es6+的代碼能不能動態加載呢?是的,可是這樣有什麼意義呢?es6 會更快嗎?

咱們得首先明確一點,通常狀況下在新標準發佈後,瀏覽器廠商會着重優化新標準的性能,而老的標準的性能優化會逐漸停滯,即便面向將來編程,es6 的性能也會往愈來愈快的方向發展.
其次,咱們平時編寫的代碼可都es6+,而發佈的es5是通過babel 或者 ts 轉譯的,在大多數狀況下,通過工具轉譯的代碼每每被比不上手寫代碼的性能,這個性能對比網站 的顯示也是如此,雖然 babel 等轉譯工具都在進步,可是仍然會看到轉譯後代碼的性能降低,尤爲是對 class 代碼的轉譯,其性能降低是很明顯的.
最後,轉譯後的代碼體積會出現代碼膨脹的狀況,轉譯器用了不少奇技淫巧將 es6 轉爲 es5 致使了代碼量劇增,使用 es6就表明了更小的體積.

那麼如何動態加載 es6 代碼呢?祕訣就是<script type="module">這個標籤來判斷瀏覽器是否支持 es6,我以前在掘金上看到了一篇翻譯的文章 有詳細的動態打包過程,能夠拓展閱讀.

體積大小對比

image.png

執行時間對比

image.png
雙方對比的結果是,es6 的代碼體積在小了一倍的同時,性能高出一倍.

1.5.4 路由級別拆解代碼

咱們在上文中已經經過SplitChunksPlugin將第三方庫進行了抽離,可是在首屏加載過程當中依然有不少冗餘代碼,好比咱們的首頁是個登陸界面,那麼其實用到的代碼很簡單

  1. 框架的基礎庫例如 vue redux 等等
  2. ui 框架的部分 form 組件和按鈕組件等等
  3. 一個簡單的佈局組件
  4. 其它少許邏輯和樣式

登陸界面的代碼是不多的,爲何不僅加載登陸界面的代碼呢?
這就須要咱們進行對代碼在路由級別的拆分,除了基礎的框架和 UI 庫以外,咱們只須要加載當前頁面的代碼便可,這就有得用到Code Splitting技術進行代碼分割,咱們要作的其實很簡單.
咱們得先給 babel 設置plugin-syntax-dynamic-import這個動態import 的插件,而後就能夠就函數體內使用 import 了.

對於Vue 你能夠這樣引入路由

export default new Router({ 
  routes: [ 
  { 
  path: '/', 
  name: 'Home', 
  component: Home 
  }, 
  { 
  path: '/login', 
  name: 'login', 
  component: () => import('@components/login') 
  } 
  ]

你的登陸頁面會被單獨打包.
對於react,其內置的 React.lazy() 就能夠動態加載路由和組件,效果與 vue 大同小異,固然 lazy() 目前尚未支持服務端渲染,若是想在服務端渲染使用,能夠用React Loadable.

2 組件加載

路由實際上是一個大組件,不少時候人們忽略了路由跳轉之間的加載優化,更多的時候咱們的精力都留在首屏加載之上,可是路由跳轉間的加載一樣重要,若是加載過慢一樣影響用戶體驗。

咱們不可忽視的是在不少時候,首屏的加載反而比路由跳轉要快,也更容易優化。

好比石墨文檔的首頁是這樣的:
image.png
一個很是常見的官網首頁,代碼量也不會太多,處理好第三方資源的加載後,是很容易就達到性能要求的頁面類型.
加載過程不過幾秒鐘,而當我跳轉到真正的工做界面時,這是個相似 word 的在線編輯器
image.png
我用 Lighthouse 的測試結果是,可交互時間高達17.2s
image.png

這並非石墨作得不夠好,而是對於這種應用型網站,相比於首屏,工做頁面的跳轉加載優化難度更大,由於其工做頁面的代碼量遠遠大於一個官網的代碼量和複雜度.

咱們看到在加載過程當中有超過 6000ms 再進行 JavaScript 的解析和執行

image.png

2.1 組件懶加載

Code Splitting不只能夠進行路由分割,甚至能夠進行組件級別的代碼分割,固然是用方式也是大同小異,組件的級別的分割帶來的好處是咱們能夠在頁面的加載中只渲染部分必須的組件,而其他的組件能夠按需加載.

就好比一個Dropdown(下拉組件),咱們在渲染初始頁面的時候下拉的Menu(菜單組件)是不必渲染的,由於只有點擊了Dropdown以後Menu 纔有必要渲染出來.

路由分割 vs 組件分割

image.png
咱們能夠以一個demo 爲例來分析一下組件級別分割的方法與技巧.
咱們假設一個場景,好比咱們在作一個打卡應用,有一個需求是咱們點擊下拉菜單選擇相關的習慣,查看近一週的打卡狀況.

咱們的 demo 是這樣子:

QQ20190611-105233.gif

咱們先對比一下有組件分割和無組件分割的資源加載狀況(開發環境下無壓縮)

無組件分割,咱們看到有一個很是大的chunk,由於這個組件除了咱們的代碼外,還包含了 antd 組件和 Echarts 圖表以及 React 框架部分代碼

image.png

組件分割後,初始頁面體積降低明顯,路由間跳轉的初始頁面加載體積變小意味着更快的加載速度

image.png

其實組件分割的方法跟路由分割差很少,也是經過 lazy + Suspense 的方法進行組件懶加載

// 動態加載圖表組件
const Chart = lazy(() => import(/* webpackChunkName: 'chart' */'./charts'))

// 包含着圖表的 modal 組件
const ModalEchart = (props) => (
    <Modal
    title="Basic Modal"
    visible={props.visible}
    onOk={props.handleOk}
    onCancel={props.handleCancel}
  >
      <Chart />
  </Modal>
)

2.2 組件預加載

咱們經過組件懶加載將頁面的初始渲染的資源體積下降了下來,提升了加載性能,可是組件的性能又出現了問題,仍是上一個 demo,咱們把初始頁面的 3.9m 的體積減小到了1.7m,頁面的加載是迅速了,可是組件的加載卻變慢了.

緣由是其他的 2m 資源的壓力所有壓在了圖表組件上(Echarts 的體積緣故),所以當咱們點擊菜單加載圖表的時候會出現 1-2s 的 loading 延遲,以下:

image.png

咱們能不能提早把圖表加載進來,避免圖表渲染中加載時間過長的問題?這種提早加載的方法就是組件的預加載.

原理也很簡單,就是在用戶的鼠標還處於 hover 狀態的時候就開始觸發圖表資源的加載,一般狀況下當用戶點擊結束以後,加載也基本完成,這個時候圖表會很順利地渲染出來,不會出現延遲.

/**
 * @param {*} factory 懶加載的組件
 * @param {*} next factory組件下面須要預加載的組件
 */
function lazyWithPreload(factory, next) {
  const Component = lazy(factory);
  Component.preload = next;
  return Component;
}
...
// 而後在組件的方法中觸發預加載
  const preloadChart = () => {
    Modal.preload()
  }

demo地址

2.3 keep-alive

對於使用 vue 的開發者 keep-alive 這個 API 應該是最熟悉不過了,keep-alive 的做用是在頁面已經跳轉後依然不銷燬組件,保存組件對應的實例在內存中,當此頁面再次須要渲染的時候就能夠利用已經緩存的組件實例了。

若是大量實例不銷燬保存在內存中,那麼這個 API 存在內存泄漏的風險,因此要注意調用deactivated銷燬

可是在 React 中並無對應的實現,而官方 issue 中官方也明確不會添加相似的 API,可是給出了兩個自行實現的方法

  1. 利用全局狀態管理工具例如 redux 進行狀態緩存
  2. 利用 style={{display: 'none'}} 進行控制

若是你看了這兩個建議就知道不靠譜,redux 已經足夠囉嗦了,咱們爲了緩存狀態而利用 redux 這種全局方案,其額外的工做量和複雜度提高是得不償失的,用 dispaly 控制顯示是個很簡單的方法,可是也足夠粗暴,咱們會損失不少可操做的空間,好比動畫。

react-keep-alive 在必定程度上解決這個問題,它的原理是利用React 的 Portals API 將緩存組件掛載到根節點之外的 dom 上,在須要恢復的時候再將緩存組件掛在到相應節點上,同時也能夠在額外的生命週期 componentWillUnactivate 進行銷燬處理。


小結

固然還有不少常見的性能優化方案咱們沒有說起:

  1. 圖片懶加載方案,這是史前前端就開始用的技術,在 JQuery 或者各類框架都有成熟方案
  2. 資源壓縮,如今基本上用反向代理工具都是自動開啓的
  3. cdn,已經見不到幾個web 產品不用 cdn 了,尤爲是雲計算廠商崛起後 cdn 很便宜了
  4. 域名收斂或者域名發散,這種狀況在 http2 使用以後意義有限,由於一個域名能夠直接創建雙向通道多路複用了
  5. 雪碧圖,很古老的技術了,http2 使用後也是效果有限了
  6. css 放頭,js 放最後,這種方式適合工程化以前,如今基本都用打包工具代替了
  7. 其它...

咱們着重整理了前端加載階段的性能優化方案,不少時候只是給出了方向,真正要進行優化仍是須要在實際項目中根據具體狀況進行分析挖掘才能將性能優化作到最好.


參考連接:

  1. 性能指標的參考
  2. Tree Shaking原理
  3. 組件預加載
  4. http2
  5. 部署 es6
  6. Tree-Shaking性能優化實踐
  7. 緩存策略

公衆號

想要實時關注筆者最新的文章和最新的文檔更新請關注公衆號程序員面試官,後續的文章會優先在公衆號更新.

簡歷模板: 關注公衆號回覆「模板」獲取

《前端面試手冊》: 配套於本指南的突擊手冊,關注公衆號回覆「fed」獲取

2019-08-12-03-18-41

本文由博客一文多發平臺 OpenWrite 發佈!
相關文章
相關標籤/搜索