? 記一次前端效能優化

小蘿蔔丁發表於2019-05-10

工作中一直在做一款公司內部的BI工具,將資料視覺化的報表賦能給業務人員,報表配置者通過簡單的拖拽操作即可生成報表。隨著系統不斷的完善,加上運維推廣,我們積累了越來越多的使用者。這時候使用者體驗的方方面面都體現出來了。我們也停下產品的功能迭代,將整個系統進行優化,旨在提升使用者體驗。以下是我對前端專案的優化總結。

Webpack 打包優化

專案中在使用的 Webpack 版本是3.x,本次優化的方案仍然是基於Webpack3.x版本的 Vue 腳手架進行優化。升級4.x在計劃中。。。

之前也總結過一次 Webpack 2.x 在Vue2.x專案中的應用,提到過 Webpack 工程的一些優化方案,以下算是一個補充。

開啟Gzip

嘗試了下開啟gzip,直接受益還是比較大的。下面是實際專案中打包結果。

  • Parsed的js,1.38M

parsed-js

  • Gizpped的js - 421.46K

gzipped-js

Webpack__Gzipped_

通過資料分析,減少了**70.28%**的打包體積。

開啟方式,在腳手架中修改配置檔案:/config/index.js

// 生產模式
build: {
  productionGzip: true // 開啟Gzip壓縮
}
複製程式碼

同時服務端 nginx 加入配置項

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 6;
gzip_types application/javascript text/plain application/x-javascript text/css application/xml text/javascript application/json;
gzip_vary on;
複製程式碼

重啟 nginx 後重新整理頁面,在Chrome develop toolsNetwork 檢視網路連結

Request Headers 中出現 Accept-Encoding: gzip 代表客戶端能夠理解 gzip 壓縮編碼方式

gzip_network

Response Headers 中出現 Content-Encoding 代表服務端指明以 gzip 編碼方式對資料進行壓縮

gzip_network

這一對請求頭部關鍵字搭配出現,說明配置成功。

使用 Preload 外掛

preload-webpack-plugin

? 使用 Resource Hints 中的 preloadprefetch 來提升應用的效能。

關於 preloadprefetch

<link rel="preload"> 是一種 resource hint,用來指定頁面載入後很快會被用到的資源,所以在頁面載入的過程中,我們希望在瀏覽器開始主體渲染之前儘早 preload。

<link rel="prefetch"> 是一種 resource hint,用來告訴瀏覽器在頁面載入完成後,利用空閒時間提前獲取使用者未來可能會訪問的內容。

在 Webpack 中配置 preload

preload-webpack-pluginhtml-webpack-plugin 外掛的一個擴充套件,所以需要搭配使用。

例如配置 preload:

plugins: [
  new HtmlWebpackPlugin(),
  new PreloadWebpackPlugin({
    rel: 'preload',
    as(entry) {
      if (/\.css$/.test(entry)) return 'style';
      if (/\.woff$/.test(entry)) return 'font';
      if (/\.png$/.test(entry)) return 'image';
      return 'script';
    },
    include: ['app']
  })
]
複製程式碼

最終在html注入為:

<link rel="preload" as="script" href="app.31132ae6680e598f8879.js">
複製程式碼

在 Webpack 中配置 prefetch

prefetch 配合 Vue 中的路由懶載入程式碼分割更好用

因為本專案視覺化工具中沒有使用路由,沒有配置prefetch

優化package

目前專案中比較常用的工具類庫有 lodash、moment、element-ui,對於這些經常使用的類庫可以通過 Dllplugin 分離依賴成一個靜態資源庫。一般不會去改動這個依賴包版本。

不過像lodash、moment是有其他方法來減少打包體積的。

  • 按需載入 element-ui,見官方文件

  • 按需載入 lodash

一般我們使用 lodash 時,不會用到其中所有的函式。有可能用到了幾個,這時候可以選擇按需引入 lodash,不要引入全量。下面通過安裝兩個外掛:

npm i babel-plugin-lodash lodash-webpack-plugin -D
複製程式碼

配置 .babelrc 檔案

"plugins": [
  "lodash"
]
複製程式碼
  • 使用 dayjs 代替 moment,API基本一樣,使用後會發現大部分場景都能使用,而且打包只有 7KB

升級 HTTP2

視覺化工具中元件變得越來越豐富,隨之帶來的頁面請求資料介面也逐漸變多,開銷在逐漸增大。單個頁面資料介面請求幾十上百不等。

如果繼續使用HTTP1.x,大家都懂的,HTTP1.x協議的侷限性,大多數現代瀏覽器都支援同時一個主機最大請求數量為6個,也就是說,如果這6個介面請求沒有返回結果處於pending狀態的話,頁面就一直刷不出資料,這樣給使用者的體驗是很差的。HTTP2的多路複用解決了這個問題,我們通過將伺服器升級為 HTTP2 增大了瀏覽器請求連線吞吐量,大大提升了應用的效能。

HTTP2 簡介

HTTP2.0 可以讓我們的應用更快、更簡單、更健壯 --- 《Web效能權威指南》

HTTP 2.0 的目的就是通過支援請求與響應的多路複用來減少延遲,通過壓縮 HTTP 首部欄位將協議開銷降至最低,同時增加對請求優先順序和伺服器端推送的支援。

HTTP 2.0 效能增強的核心,全在於新增的二進位制分幀層,它定義瞭如何封裝 HTTP 訊息並在客戶端與伺服器之間傳輸。

HTTP 2.0 把 HTTP 協議通訊的基本單位縮小為一個一個的幀,這些幀對應著邏輯流中的訊息。相應地,很多流可以並行地在同一個 TCP 連線上交換訊息。

HTTP 2.0 的二進位制分幀機制解決了 HTTP 1.x 中存在的隊首阻塞問題, 也消除了並行處理和傳送請求及響應時對多個連線的依賴。結果,就是應用速度更快、開發更簡單、部署成本更低。

HTTP2 優化

  • 域名分割槽在 HTTP 2.0 之下屬於反模式,因為多個連線會抵消新協議中首部壓縮和請求優先順序的效用
  • 去掉不必要的資源打包,例如生成雪碧圖,支援了 HTTP 2.0,很多小資源都可以並行傳送,導致打包資源的效率反而更低
  • 使用客戶端快取應用資源
  • 部署 HTTP 2.0 的同時部署TLS協議(傳輸層安全協議),即HTTPS

使用 HTTP 快取

快取應用資源,避免每次請求都傳送相同的內容。瀏覽器在下載靜態資源後,使用快取將下載過的資源維護好,這樣下次載入網頁時直接使用本地的副本。減少了資源請求以及等待時間。

Cache-Control

通用的HTTP請求頭首部欄位,只需指定一個明確的快取時間即可。可以配置在 nginx 配置檔案裡。

location ~ .*\.(js|css|ttf|svg|ico){
    add_header Cache-Control  max-age=86400;
}
複製程式碼

頁面第一次載入

快取前

再次載入

快取後

快取驗證

快取驗證

可以看到加入快取後,Status Code 為 200 OK (from memory cache),快取時間為:max-age=86400

Vue 批量渲染元件

業務場景中,隨著應用變得越來越複雜,載入一個頁面可能需要渲染過多的元件,渲染多個元件有兩種策略:

  • 遍歷所有元件,每一個介面請求返回資料時去渲染元件
  • 請求所有介面,所有資料返回時批量渲染元件

通過實踐發現,後者渲染更快,後者消除了每次請求介面之後渲染元件的時間,因為多次渲染元件會帶來額外的Scripting開銷,比如Vue中的 computedwatch;同時結合 HTTP2 的多路複用,請求多個介面也會很快的響應。

示例程式碼:

// 批量更新元件方法
batchUpdateComponent({ dispatch }, promises) {
  // 請求所有介面
  return Promise.all(promises.map(p => p.catch(() => undefined)))
    .catch(err => {
      console.log(err)
    })
    .then(res => {
      // 一次性渲染元件
      res && dispatch('updateComponent', res)
    })
}
複製程式碼

? 如果 Promise 的 catch 回撥返回了 undefined,那麼 Promise 的失敗就會被當做成功來處理。 使用 ES2018 的提案 Promise.finally

Vue 非同步元件

專案中應用業務程式碼量在不斷攀升,寫了很多業務元件,其實在一定場景下,並非所有元件都需要渲染,比如,視覺化工具有編輯模式和預覽模式。編輯模式需要使用 Code Mirror 用來編寫一些 SQL 語句,預覽模式時候就不需要使用。

元件正常引入:

import CustomSql from '@/components/CustomSql'

export default {
  components: {
    CustomSql
  }
}
複製程式碼

元件非同步引入:

// ES6 結合 Webpack 
export default {
  components: {
    CustomSql: () => import('./CustomSql')
  }
}
複製程式碼

Vue中路由懶載入就是使用非同步元件Webpack程式碼分割功能實現的。

SVG優化

隨著專案中元件的增多,元件的icon隨之也變的多了。大部分icon是svg格式,我們可以使用 SVG Sprite 技術管理SVG圖示。

SVG Sprite 技術

所謂 SVG Sprite 類似於CSS中的Sprite技術。將圖示整合在一起,實際呈現的時候準確顯示特定圖示。

SVG Sprite 技術最佳實踐是:

  • 使用 symbol 元素整合圖示
  • 使用 use 元素來使用圖示

使用例子:

<svg>
	<!-- symbol definition  NEVER draw -->
	<symbol id="sym01" viewBox="0 0 150 110">
	  <circle cx="50" cy="50" r="40" stroke-width="8" stroke="red" fill="red"/>
	  <circle cx="90" cy="60" r="40" stroke-width="8" stroke="green" fill="white"/>
	</symbol>
	
	<!-- actual drawing by "use" element -->
	<use xlink:href="#sym01"
	     x="0" y="0" width="100" height="50"/>
	<use xlink:href="#sym01"
	     x="0" y="50" width="75" height="38"/>
	<use xlink:href="#sym01"
	     x="0" y="100" width="50" height="25"/>
</svg>
複製程式碼

元件化 SvgIcon

基於Vue封裝的 SVG ICON 元件

// @/components/SvgIcon.vue
<template>
  <svg :class="svgClass" aria-hidden="true" v-on="$listeners">
    <use :xlink:href="iconName" />
  </svg>
</template>
    
<script>
export default {
  name: 'SvgIcon',
  props: {
    iconClass: {
      type: String,
      required: true
    },
    className: {
      type: String,
      default: ''
    }
  },
  computed: {
    iconName() {
      return `#icon-${this.iconClass}`
    },
    svgClass() {
      return 'svg-icon ' + this.className
    }
  }
}
</script>
    
<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>
複製程式碼

自動化引入 SVG

將 src/assets/icons 下所有icon動態引入

// @/plugins/svgicon.js
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon'
    
Vue.component('svg-icon', SvgIcon)
    
const requireAll = requireContext => requireContext.keys().map(requireContext)
    
const svgIcons = require.context('./components', false, /\.svg$/)
requireAll(svgIcons)
複製程式碼

打包 SVG Sprite

我們可以用 svg-sprite-loader 這個外掛來生成 SVG Sprite,通過元件的方式引入 svg icon。

基於 Webpack 3.x 的配置方法如下:

// 通過 exclude/include 來區分哪些屬於svg icon,哪些屬於image
{
  test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
  loader: 'url-loader',
  exclude: [resolve('src/assets/icons')],
  options: {
    limit: 10000,
    name: utils.assetsPath('img/[name].[hash:7].[ext]')
  }
},
{
  test: /\.svg$/,
  loader: 'svg-sprite-loader',
  include: [resolve('src/assets/icons')],
  options: {
    symbolId: 'icon-[name]'
  }
}
複製程式碼

總結

本次效能優化關鍵點:

Webpack方面:

  • 開啟Gzip,直接收益比較大
  • 使用preload外掛,預先宣告要使用到的資源
  • 儘可能優化package,做到按需載入,減少打包體積

網路方面:

  • 升級伺服器為HTTP2,結合HTTPS是最佳實踐
  • 使用 HTTP 快取策略,最好的效能是不用請求

Vue實踐方面:

  • 渲染元件時機,建議在全部介面請求返回後去批量渲染
  • 將不常用的特定場景下使用的元件寫成非同步元件

資源方面:

  • 專案中使用較多SVG時,可以選擇使用“SVG Sprite”技術管理

最後

專案初始,由於工期緊張,我們急著迭代功能,目標是交付功能完備的應用,使用者量增長的時候就該停下來好好考慮考慮如何提升應用的效能了。縱使應用的功能再完備,如果使用者體驗非常差,那是不是值得反思,效能優化是一件需要持續做的事情。

我想借用一下《Web效能權威指南》裡,Ilya Grigorik 提到的:“?我們關心的不止是交付能用的應用,我們目標是交付最佳效能!” 來總結效能優化的實踐,同時提醒自己,在做專案的時候儘可能的提前想到效能優化的點。

參考

《Web效能權威指南》

原文? 記一次前端效能優化

相關文章