全文共6511字/詞,閱讀大概須要13分鐘,太長不看黨請直接移步👉「開始優化」部分直接查看優化手段javascript
前段時間公司服務器網絡波動,網站訪問變慢,一些性能問題也隨之暴露了出來。紛紛反饋在這樣的弱網條件下,訪問新項目時,加載了近1分鐘都沒加載出來,而訪問其餘頁面頂多也就30-40s。css
在網絡恢復後,嘗試訪問了下頁面,無緩存首次打開須要等待近11s的時間,最大的資源達到了3.7M...html
在對項目作了一些優化處理後,再次無緩存打開能夠發現網頁幾乎是秒開,平均耗時在1s之內前端
在這裏總結記錄一下,基本上都是一些常規可複製的優化手段,但願能爲一樣想優化網頁性能的你提供思路~vue
Slow3G條件下22-25s加載完成java
在開始以前,咱們須要明白一個原則:性能優化的最終目的是提高用戶體驗。
簡而言之就是讓用戶感受這個網站很「快」(至少不慢hh),這裏的「快」有兩種,一種是「真的快」一種是「以爲快」node
作好這兩方面都能提高用戶對網站的性能評價。webpack
另外就是軟件工程沒有銀彈,一種優化方案可能適用於大多數項目,可是某些特殊狀況下極可能會起反效果。nginx
舉個🌰,因爲瀏覽器有單域名下併發請求限制,一般咱們會將依賴統一打成一個vendor包(vue-cli默認策略),減小首屏請求數,且依賴不變更的狀況下文件指紋不變,能夠有效利用304緩存。在依賴很少的狀況這麼處理確實有助於提高加載速度,但一旦依賴多起來,vendor就會特別的大,在弱網條件下,會嚴重拖慢頁面顯示。這顯然不是咱們想要的,因此咱們根據狀況會對vendor進行拆分,好比拆分到CDN,或者直接拆分到頁面中git
所以,咱們在作性能優化過程當中,必須根據最終能給用戶體驗帶來的提高權衡後作出適合當前項目的選擇
目標會影響咱們在過程當中的決策
指標則用來度量咱們的目標
首先咱們須要肯定目標,根據場景和項目複雜度不一樣,制定的目標也不一樣,好比但願比競品快20%,或者符合標準的"2-5-10"原則等等
這裏我定下的目標是
關於指標這塊,簡單介紹下常見指標
經過性能調試工具能夠直觀便捷地獲取這些指標,好比Newwork、k六、hiper、Lighthouse...。具體能夠看我關於性能調試工具的另外一篇文章
從Network上咱們發現主要問題在3.2M的chunk-vendor.js上
因爲本次不涉及到應用內場景性能優化,Performance分析跳過...
從webpack bundle能夠看出,問題着實很多
🛫🛫🛫
before:4.96M after:4.12M
每次使用在線服務手動壓縮較爲麻煩,能夠直接在構建流程中加入壓縮圖片
// install
npm i image-webpack-loader -D
// vue.config.js
chainWebpack: (config) => {
if (isProd) {
// 圖片壓縮處理
const imgRule = config.module.rule('images')
imgRule
.test(/\.(png|jpe?g|gif|webp)(\?.*)?$/)
.use('image-webpack-loader')
.loader('image-webpack-loader')
.options({ bypassOnDebug: true })
.end()
}
}
複製代碼
- install或build時若是出現imagemin庫下載失敗,能夠嘗試 換源、配置github hosts、install時添加
--user=root
解決- 因爲在圖片下載後已經手動用在線工具壓縮過,這部分提高不大
before:4.12M after:4.00M
webP是谷歌推出的新圖片格式(2010),同等質量下體積拳打png腳踢jpg,目前兼容性還算能夠,就蘋果家的表現不太理想
能夠手動,也能夠加入構建自動化生成。
./cwebp -q 75 login_plane_2.png -o login_plane_2.webp
複製代碼
<picture>
標籤兼容<picture>
<source srcset="hehe.webp" type="image/webp">
<img src="hehe.png" alt="hehe>
</picture>
複製代碼
// main.js
window.addEventListener('DOMContentLoaded', () => {
const isSupportWebP = document.createElement('canvas')
.toDataURL('image/webp')
.indexOf('data:image/webp') === 0
document.documentElement.classList.add(isSupportWebP ? '' : '.no-support-webp');
})
// css
.support-webp .bg{
background-image: url("hehe.webp");
}
.no-support-webp .bg {
background-image: url("hehe.png");
}
複製代碼
- 請務必使用原圖進行webp轉換,不然會影響體積
- 項目大圖很少,最大400KB的圖片轉換後只有48.9KB
這一步咱們來優化部分冗餘的舊SVG圖標被打包進去的狀況,通常項目中SVG使用方式都是在iconfont生成JS而後引入。這種作法
要實現上述效果,只須要
// install
npm i svg-sprite-loader -D
// vue.config.js
chainWebpack: (config) => {
// SVG處理
config.module
.rule('svg')
.exclude.add(resolve('src/icons/svg'))
.end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('src/icons/svg'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'icon-[name]'
})
.end()
}
複製代碼
// src/icons/index.js
const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)
// main.js
import '@/icons'
複製代碼
// src/icons/index.js
import Vue from 'vue'
import CaSVG from '@/components/ca-svg'
Vue.component('ca-svg', CaSVG)
// src/components/ca-svg.vue
<template>
<svg :class="svgClass" aria-hidden="true" v-on="$listeners" :style="svgStyle">
<use :xlink:href="iconName" />
</svg>
</template>
...
// name屬性爲必須字段,其餘size或color能夠自由定製
複製代碼
SVG一般會有一些冗餘信息致使影響體積,這裏咱們可使用svgo-loader來進一步壓縮
// install
npm i svgo-loader -D
// vue.config.js
// 接上面svg的配置
...
.end()
.use('svgo-loader')
.loader('svgo-loader')
.end()
複製代碼
能夠看到這部分在chunk-vendor中佔的比例不小
這部分實際上已是作了處理的了,具體操做參考ant-design-vue文檔,按說明作沒啥大坑,效果也符合預期。
部分組件是不常常用的,但卻使用了Vue.use()全局引入了。這裏去掉不經常使用和沒用到的全局引入,改成頁面內import()引入
這一部分,因爲咱們在項目中只使用了幾個Ant內置圖標,不可能有530+KB。根據Ant文檔的描述是因爲其將ICON全量引入的關係致使的,說法是當前用法若是按需加載的話沒法肯定使用者會不會在運行時改變icon,好比配置的ICON。
這個問題,在React版的Ant-Design是已是作了處理的了(寫法上有所調整),但在Ant-Design-Vue-1.x中仍然沒有官方解決方案。目前瞭解到的有兩種方案
@ant-design/icons/lib/dist
指向項目中的antd-icon.js
,而後在antd-icon
中按需導出便可// alias配置
resolve: {
alias: {
'@ant-design/icons/lib/dist$': path.resolve(__dirname, './src/icons/antd-icon.js')
}
}
// src/icons/antd-icon.js
export { default as LoadingOutline } from '@ant-design/icons/lib/outline/LoadingOutline'
複製代碼
- 注意項目中和Ant組件中使用的ICON都要導出
- 優化後從530K+到30K+
這兩個包300K + 160K,加起來有460K,也是佔的比較多的一項
這裏使用內置的IgnorePlugin便可作到
// webpack plugins
plugins: [
// Ignore all locale files of moment.js
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
],
複製代碼
moment-timezone包含了moment,項目中只有一處地方使用到,用來獲取東八區的當前時間,moment是能夠作到的,不須要額外引入moment-timezone
// old
moment.tz('Asia/Shanghai').format('YYYYMMDDHHmmss')
// new
moment.utcOffset(480).format('YYYYMMDDHHmmss')
複製代碼
dayjs實現了moment的大多數功能,Api基本一致,壓縮後體積卻不到2k,是優秀的替代方案,且多數狀況下,dayjs能夠完美的替代moment(按需引入插件)
可是這裏遇到了使用Ant-Design的第二個坑,Ant-Design實現Date-Picker時使用了moment,因此在項目中不使用moment也無論用,同樣會打包進來....
這個問題,在React版的Ant-Design又已是作了處理的了,容許用戶選擇dayjs或moment。一樣的,Ant-Design-Vue仍沒有跟進...
所幸,dayjs做者提供了一個插件,能夠將Ant-Design-Vue的moment替換成dayjs👍 雖然文檔中只說Ant-Design-React能夠用,但實際上在issue能夠看到它也適用於antdV,不過須要調整一些配置
// vue.config.js
const AntdDayjsWebpackPlugin = require('antd-dayjs-webpack-plugin')
configureWebpack: {
...
plugins: [
new AntdDayjsWebpackPlugin({ preset: 'antdv3', replaceMoment: true })
],
}
複製代碼
replaceMoment參數是用webpack alias來重定向moment到dayjs,因爲項目中多處使用moment,這樣作能夠繼續使用moment實際上調用的dayjs,實現無感知替換👍
- 替換成dayjs後,一些功能須要經過引入插件來保持一致,好比utc、updateLocale
- 替換成dayjs後,一些針對moment的優化須要改爲dayjs或去掉
core-js實際上就是瀏覽器新API的polyfill,項目是PC端,因此主要是爲了兼容IE...
項目中默認是useBuiltIns: 'entry'
將全部polyfill都引入了,致使包比較大。咱們可使用useBuiltIns: 'entry'
調整下策略,按需引入,項目中沒使用到的API就不作polyfill處理了
// babel.config.js
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
[
'@babel/preset-env',
{
'useBuiltIns': 'usage', // entry,usage
'corejs': 3
}
]
],
plugins
}
複製代碼
btw,這裏更優的作法應該是使用動態polyfill,讓服務器根據UA判斷是否要返回polyfill
vue-cli3的默認優化是將全部npm依賴都打進chunk-vendor,但這種作法在依賴多的狀況下致使chunk-vendor過大
optimization: isProd ? {
splitChunks: {
chunks: 'all',
maxInitialRequests: Infinity, // 默認爲3,調整爲容許無限入口資源
minSize: 20000, // 20K如下的依賴不作拆分
cacheGroups: {
vendors: {
// 拆分依賴,避免單文件過大拖慢頁面展現
// 得益於HTTP2多路複用,不用太擔憂資源請求太多的問題
name(module) {
// 拆包
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
// 進一步將Ant組件拆分出來,請根據狀況來
// const packageName = module.context.match(/[\\/]node_modules[\\/](?:ant-design-vue[\\/]es[\\/])?(.*?)([\\/]|$)/)[1]
return `npm.${packageName.replace('@', '')}` // 部分服務器不容許URL帶@
},
test: /[\\/]node_modules[\\/]/,
priority: -10,
chunks: 'initial'
}
}
},
runtimeChunk: { name: entrypoint => `runtime-${entrypoint.name}` }
} : {}
複製代碼
- Tips: vue inspect > output.js --mode production 能夠查看最終配置
- 分包這塊須要根據實際狀況作對應處理,才能取得比較好的效果,總之多看文檔多試就對了
SPA中一個很重要的提速手段就是路由懶加載,當打開頁面時纔去加載對應文件,咱們利用Vue的異步組件和webpack的代碼分割(import()
)就能夠輕鬆實現懶加載了。
但當路由過多時,請合理地用webpack的魔法註釋對路由進行分組,太多的chunk會影響構建時的速度
{
path: 'register',
name: 'register',
component: () => import(/* webpackChunkName: "user" */ '@/views/user/register'),
}
複製代碼
請只在生產時懶加載,不然路由多起來後,開發時的構建速度感人
HTTP2是HTTP協議的第二個版本,相較於HTTP1 速度更快、延遲更低,功能更多。 目前來看兼容性方面也算過得去,在國內有超過50%的覆蓋率。
一般瀏覽器在傳輸時併發請求數是有限制的,超過限制的請求須要排隊,以往咱們經過域名分片、資源合併來避開這一限制,而使用HTTP2協議後,其能夠在一個TCP鏈接分幀處理多個請求(多路複用),不受此限制。(其他的頭部壓縮等等也帶來了必定性能提高)
若是網站支持HTTPS,請一併開啓HTTP2,成本低收益高,對於請求多的頁面提高很大,尤爲是在網速不佳時
// nginx.conf
listen 443 http2;
複製代碼
nginx -s stop && nginx
複製代碼
多路複用避開了資源併發限制,但資源太多的狀況,也會形成瀏覽器性能損失(Chrome進程間通訊與資源數量相關)
Gzip壓縮是一種強力壓縮手段,針對文本文件時一般能減小2/3的體積。
HTTP協議中用頭部字段Accept-Encoding
和 Content-Encoding
對「採用何種編碼格式傳輸正文」進行了協定,請求頭的Accept-Encoding
會列出客戶端支持的編碼格式。當響應頭的 Content-Encoding
指定了gzip時,瀏覽器則會進行對應解壓
通常瀏覽器都支持gzip,因此Accept-Encoding
也會自動帶上gzip
,因此咱們須要讓資源服務器在Content-Encoding
指定gzip,並返回gzip文件
#開啓和關閉gzip模式
gzip on;
#gizp壓縮起點,文件大於1k才進行壓縮
gzip_min_length 1k;
# gzip 壓縮級別,1-9,數字越大壓縮的越好,也越佔用CPU時間
gzip_comp_level 6;
# 進行壓縮的文件類型。
gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript ;
# nginx對於靜態文件的處理模塊,開啓後會尋找以.gz結尾的文件,直接返回,不會佔用cpu進行壓縮,若是找不到則不進行壓縮
gzip_static on
# 是否在http header中添加Vary: Accept-Encoding,建議開啓
gzip_vary on;
# 設置gzip壓縮針對的HTTP協議版本
gzip_http_version 1.1;
複製代碼
雖然上面配置後Nginx已經會在響應請求時進行壓縮並返回Gzip了,可是壓縮操做自己是會佔用服務器的CPU和時間的,壓縮等級越高開銷越大,因此咱們一般會一併上傳gzip文件,讓服務器直接返回壓縮後文件
// vue.config.js
const CompressionPlugin = require('compression-webpack-plugin')
// gzip壓縮處理
chainWebpack: (config) => {
if(isProd) {
config.plugin('compression-webpack-plugin')
.use(new CompressionPlugin({
test: /\.js$|\.html$|\.css$/, // 匹配文件名
threshold: 10240, // 對超過10k的數據壓縮
deleteOriginalAssets: false // 不刪除源文件
}))
}
}
複製代碼
- 插件的默認壓縮等級是9,最高級的壓縮
- 圖片文件不建議使用gzip壓縮,效果較差
<link>標籤的rel屬性的兩個可選值。
Prefetch,預請求,是爲了提示瀏覽器,用戶將來的瀏覽有可能須要加載目標資源,因此瀏覽器有可能經過事先獲取和緩存對應資源,優化用戶體驗。
Preload,預加載,表示用戶十分有可能須要在當前瀏覽中加載目標資源,因此瀏覽器必須預先獲取和緩存對應資源。
Prefetch、Preload能夠在某些場景下能夠有效優化用戶體驗。 舉些場景
字體樣式閃動
,而咱們使用Preload
提早加載字體後這種狀況就好不少了,大圖也是如此Vue-Cli3默認會使用preload-webpack-plugin對chunk資源作preload、prefetch處理,入口文件preload,路由chunk則是prefetch。
通常來講不須要作特別處理,若是判斷不須要或者須要調整在vue.config.js
中配置便可
- 理論上prefetch不會影響加載速度,但實際測試中,是有輕微影響的,不過這個見仁見智,我認爲整體體驗上仍是有所提高的,
- 相似字體文件這種隱藏在腳本、樣式中的首屏關鍵資源,建議使用preload
- 移動端流量訪問時慎用
當應對一些弱網地區時,OSS + CDN無疑是很強力的提速手段。
海量,安全,低成本,高可靠的雲存儲服務。能夠經過簡單的REST接口,在任什麼時候間、任何地點上傳和下載數據,也可使用WEB頁面對數據進行管理。
OSS的特色:
另外,OSS還提供一些方便的服務
這裏推薦直接購買阿里家的OSS,OSS雖然也有傳輸加速服務,但對於靜態熱點文件的下載加速場景仍是須要CDN加速
CDN加速原理是把提供的域名做爲源站,將源內容緩存到邊緣節點。當客戶讀取數據時,會從最適合的節點(通常來講就近獲取)獲取緩存文件,以提高下載速度。
因爲沒申請到資源,項目並無上OSS+CDN。但若是有條件仍是建議上,提高很大
首屏優化,在JS沒解析執行前,讓用戶能看到Loading動畫,減輕等待焦慮。一般會在index.html上寫簡單的CSS動畫,直到Vue掛載後替換掛載節點的內容,但這種作法實測也會出現短暫的白屏,建議手動控制CSS動畫關閉
首屏優化,APP內常見的加載時各部分灰色色塊。針對骨架屏頁的自動化生成,業界已有很多解決方案。
首屏之外的一些場景優化,更多相關內容好比圖片懶加載、組件懶加載等 後續文章會作介紹
通常來講,圖片加載有兩種方式,一種是自上而下掃描,一種則是原圖的模糊展現,而後逐漸/加載完清晰。前者在網速差的時候用戶體驗較差,後者的漸進/交錯式加載則能減輕用戶的等待焦慮,帶來更好的體驗
瀏覽器自己支持這種圖片的模糊到清晰的掃描加載方式,只須要將處理好資源便可
先加載小圖,模糊化渲染,圖片加載完成後替換爲原圖,最典型的例子就是Medium,模糊化能夠用filter或者canvas處理
先加載全局通用loading圖或者用CSS填充色塊,圖片加載完成後替換爲原圖。簡單粗暴,在弱網條件下頗有用
- 幾種方式能夠同時搭配使用
- 漸進/交錯格式圖片會佔用必定CPU和內存,酌情使用
弱網優化手段,用了懶加載後用戶若是在弱網條件下點擊下一個頁面在下個頁面加載完成前頁面內容不可用,用戶會理解爲卡頓。 在VueRouter的路由守衛中處理便可
本文只介紹了首屏加載場景下的性能優化,實際上性能優化遠不止這些內容,SPA的加載性能指標採集光靠Lighthouse、slow 3G模擬真的可信嗎?除了加載場景外的其餘方面呢?構建速度、操做流暢性...
性能優化影響的,不只是用戶體驗,還影響了轉化率、搜索引擎排名,這些因素都會對最終的流量、銷量等收入形成影響
來自Google的數據代表,一個有10條數據0.4秒能加載完的頁面,變成30條數據0.9秒加載完以後,流量和廣告收入降低90%。
Google Map 首頁文件大小從100KB減少到70-80KB後,流量在第一週漲了10%,接下來的三週漲了25%。
亞馬遜的數據代表:加載時間增長100毫秒,銷量就降低1%。
立場不一樣,看問題角度也會變化,好比對老闆來講最終目的實際上是搞錢hh,用戶體驗這些花裏胡哨的,別人不必定懂。
因此若是必要,請在過程先後作好性能收益的數據監控和分析,在性能優化和產品指標之間創建正向聯繫,方便自上而下的推進技術方案的執行。這纔是你說服上司或領導投入成本到性能優化上的重要依據
性能優化算是老生常談的話題了,但部分人在面對怎麼作性能優化的問題時,僅僅只是羅列出各類常見優化手段,更有深度的答案應該是遇到什麼性能問題,針對這個問題圍繞某些性能指標採起了什麼手段,手段是否帶來了其餘問題,怎麼權衡,最終達到了什麼樣的效果。