🚀 記一次前端性能優化

工做中一直在作一款公司內部的BI工具,將數據可視化的報表賦能給業務人員,報表配置者經過簡單的拖拽操做便可生成報表。隨着系統不斷的完善,加上運維推廣,咱們積累了愈來愈多的用戶。這時候用戶體驗的方方面面都體現出來了。咱們也停下產品的功能迭代,將整個系統進行優化,旨在提高用戶體驗。如下是我對前端項目的優化總結。javascript

Webpack 打包優化

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

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

開啓Gzip

嘗試了下開啓gzip,直接受益仍是比較大的。下面是實際項目中打包結果。前端

  • Parsed的js,1.38M

parsed-js

  • Gizpped的js - 421.46K

gzipped-js

Webpack__Gzipped_

經過數據分析,減小了**70.28%**的打包體積。vue

開啓方式,在腳手架中修改配置文件:/config/index.jsjava

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

同時服務端 nginx 加入配置項webpack

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 查看網絡連接nginx

Request Headers 中出現 Accept-Encoding: gzip 表明客戶端可以理解 gzip 壓縮編碼方式web

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性能權威指南》

原文🚀 記一次前端性能優化

相關文章
相關標籤/搜索