工作中一直在做一款公司內部的BI工具,將數據可視化的報表賦能給業務人員,報表配置者通過簡單的拖拽操作即可生成報表。隨着系統不斷的完善,加上運維推廣,我們積累了越來越多的用戶。這時候用戶體驗的方方面面都體現出來了。我們也停下產品的功能迭代,將整個系統進行優化,旨在提升用戶體驗。以下是我對前端項目的優化總結。
Webpack 打包優化
項目中在使用的 Webpack
版本是3.x,本次優化的方案仍然是基於Webpack3.x版本的 Vue
腳手架進行優化。升級4.x在計劃中。。。
之前也總結過一次 Webpack 2.x 在Vue2.x項目中的應用,提到過 Webpack
工程的一些優化方案,以下算是一個補充。
開啓Gzip
嘗試了下開啓gzip,直接受益還是比較大的。下面是實際項目中打包結果。
-
Parsed
的js,1.38M
-
Gizpped
的js - 421.46K
通過數據分析,減少了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 tools
中 Network
查看網絡鏈接
Request Headers
中出現 Accept-Encoding: gzip
代表客戶端能夠理解 gzip
壓縮編碼方式
Response Headers
中出現 Content-Encoding
代表服務端指明以 gzip
編碼方式對數據進行壓縮
這一對請求頭部關鍵字搭配出現,說明配置成功。
使用 Preload 插件
💡 使用 Resource Hints
中的 preload 與 prefetch 來提升應用的性能。
關於 preload
與 prefetch
<link rel="preload">
是一種 resource hint,用來指定頁面加載後很快會被用到的資源,所以在頁面加載的過程中,我們希望在瀏覽器開始主體渲染之前儘早 preload。
<link rel="prefetch">
是一種 resource hint,用來告訴瀏覽器在頁面加載完成後,利用空閒時間提前獲取用戶未來可能會訪問的內容。
在 Webpack 中配置 preload
preload-webpack-plugin
是 html-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中的 computed
或 watch
;同時結合 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性能權威指南》
- Resource Hints
- W3C Resource Hints
- Preload: What Is It Good For?
- Getting Ready For HTTP2
- HTTP 2.0 壓縮算法
- Promise.all for Rejections and Resolves
- SVG Sprite技術介紹
原文🚀 記一次前端性能優化