🚀 記一次前端性能優化

工作中一直在做一款公司內部的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性能權威指南》
原文🚀 記一次前端性能優化
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章