1. Lighthouse(谷歌瀏覽器性能分析工具)
2. 代碼分析插件(rollup-plugin-visualizer)
3. Vite 配置優化配置
4. PWA 離線存儲插件(vite-plugin-pwa)
5. 與 Vue 本身相關的優化
- 5.1 頁面加載優化
- 5.1.1 包體積與 tree-shaking 優化
- 5.1.2 代碼分割
- 5.1.3 選擇合適的架構(暫時不做探究)
- 5.2 更新優化
- 5.2.1 Prop 穩定性(儘量讓子組件的 prop 不發生改動)
- 5.2.2 v-once(元素和組件之渲染一次,跳過更新)
- 5.2.3 v-meno(有條件的跳過更新,3.2 以上版本可用)
- 5.2.4 計算屬性穩定性(返回對象時對比具體屬性,再決定是否改變)
- 5.3 通用優化
- 5.3.1 v-lazy(圖片懶加載)
- 5.3.2 大型虛擬列表(只渲染視口部分)
- 5.3.3 減少大型不可變數據的響應性開銷(使用 shallowRef() 和 shallowReactive())
- 5.3.4 避免不必要的組件抽象
- 6. 題外話
1. Lighthouse(谷歌瀏覽器性能分析工具)
Lighthouse 是谷歌瀏覽器中 devtools 中的一個性能分析工具。
選擇Desktop(桌面端),點擊 analyze page load,即可看到頁面加載的一個跑分情況。
主要參數介紹:
(1)First Contentful Paint(首屏內容繪製,FCP):首次內容繪製的時間,瀏覽器第一次繪製DOM相關的內容,也是用户第一次看到頁面內容的時間。
(2)Total Blocking Time(總阻塞時間,TBT):記錄了首次內容繪製到用户可交互之間的時間,這段時間內,主進程被阻塞,會阻礙用户的交互,頁面點擊無反應。
(3)Speed Index(速度指數):頁面各個可見部分的顯示平均時間,當我們的頁面上存在輪播圖或者需要從後端獲取內容加載時,這個數據會被影響到。
(4)Largest Contentful Paint(最大內容繪製):最大內容繪製時間,頁面最大的元素繪製完成的時間。
(5)Cumulative Layout Shift(計算佈局偏移):計算佈局偏移值得分,會比較兩次渲染幀的內容偏移情況,這個值通常為0。否則,可能出現用户想點擊A按鈕,但下一幀中,A按鈕被擠到旁邊,導致用户實際點擊了B按鈕。
2. 代碼分析插件(rollup-plugin-visualizer)
(1)下載 代碼分析插件:
npm i rollup-plugin-visualizer
(2)執行 npm run build ,會自動跳出代碼分析頁面。
可以看到,因為項目使用了完整引入 ELement Plus,所以項目打包內容十分龐大,在 1.53MB 左右。
據此,我們可以將 Element Plus 修改為按需引入,方式可參照《Vue3 組件庫 Element Plus》的 2.2.2 自動按需引入 小節。
假設我們項目只使用了el-button組件,可以看到,重新npm run build後,element-plus 的打包體積大大減小,只剩下了 18.36KB。
3. Vite 配置優化配置
vite.config.js
build: {
chunkSizeWarningLimit: 2000, // 警告單個文件大小限制,默認2000kb
cssCodeSplit: true, //css 拆分
sourcemap: false, //不生成sourcemap
minify: false, //是否禁用最小化混淆,esbuild打包速度最快,terser打包體積最小。
assetsInlineLimit: 5000 //小於該值 圖片將打包成Base64
},
4. PWA 離線存儲插件(vite-plugin-pwa)
(1)下載插件
npm i vite-plugin-pwa
(2)在 vite.config.js 中進行配置:
import { VitePWA } from 'vite-plugin-pwa'
VitePWA({
workbox: {
cacheId: "XIaoman",//緩存名稱
runtimeCaching: [
{
urlPattern: /.*\.js.*/, //緩存文件
handler: "StaleWhileRevalidate", //重新驗證時失效
options: {
cacheName: "XiaoMan-js", //緩存js,名稱
expiration: {
maxEntries: 30, //緩存文件數量 LRU算法
maxAgeSeconds: 30 * 24 * 60 * 60 //緩存有效期
}
}
}
]
},
})
(3)執行 npm run build 進行打包,生成 sw.js 文件。
(4)全局下載 http-server:
npm i -g http-server
(5)進入打包後的 dist 文件夾,本地打開:
cd dist
http-server -p 8000
5. 與 Vue 本身相關的優化
Vue 性能優化主要是在兩個方面: (1)頁面加載性能。首次訪問時,應用展示出內容與達到可交互狀態的速度。這通常會用 Google 所定義的一系列 Web 指標 (Web Vitals) 來進行衡量。比如First Contentful Paint(首屏內容繪製時間),詳情參照 1. Lighthouse(谷歌瀏覽器性能分析工具) 小節;
(2)更新性能。應用響應用户輸入更新的速度。比如當用户在搜索框中輸入時結果列表的更新速度,或者用户在一個單頁面應用 (SPA) 中點擊鏈接跳轉頁面時的切換速度。
5.1 頁面加載優化
5.1.1 包體積與 tree-shaking 優化
(1)按需引入。 採用現代化的打包工具,會支持自動化的tree-shaking(沒用過的代碼不會被打包)。所以最好許多函數庫或者組件庫採用按需引入的方式會更合適。
(2)避免引入過重依賴。 比如 lodash 默認是 commonjs 版本,對 tree-shaking 很不友好,所以可以使用 lodash-es (ES 版本)進行替代,支持tree-shaking。
5.1.2 代碼分割
代碼分割是指構建工具將構建後的 JavaScript 包拆分為多個較小的,可以按需或並行加載的文件。通過適當的代碼分割,頁面加載時需要的功能可以立即下載,而額外的塊只在需要時才加載,從而提高性能。
像 Rollup (Vite 就是基於它之上開發的) 或者 webpack 這樣的打包工具可以通過分析 ESM 動態導入的語法來自動進行代碼分割:
// lazy.js 及其依賴會被拆分到一個單獨的文件中
// 並只在 `loadLazy()` 調用時才加載
function loadLazy() {
return import('./lazy.js')
}
使用懶加載 + 異步組件
懶加載對於頁面初次加載優化極大。可與 Vue 異步組件進行搭配使用,為組件創建分離的代碼塊:
import { defineAsyncComponent } from 'vue'
// 會為 Foo.vue 及其依賴創建單獨的一個塊
// 它只會按需加載
// (即該異步組件在頁面中被渲染時)
const Foo = defineAsyncComponent(() => import('./Foo.vue'))
5.1.3 選擇合適的架構(暫時不做探究)
如果你的用例對頁面加載性能很敏感,請避免將其部署為純客户端的 SPA,而是讓服務器直接發送包含用户想要查看的內容的 HTML 代碼。純客户端渲染存在首屏加載緩慢的問題,這可以通過服務器端渲染 (SSR) 或靜態站點生成 (SSG) 來緩解。查看 SSR 指南以瞭解如何使用 Vue 實現 SSR。如果應用對交互性要求不高,你還可以使用傳統的後端服務器來渲染 HTML,並在客户端使用 Vue 對其進行增強。
不過,我們目前的學習進程都是純客户端的SPA,其他架構的內容暫時不做探究。
5.2 更新優化
5.2.1 Prop 穩定性(儘量讓子組件的 prop 不發生改動)
子組件中任意一個 prop 更新,整個組件就會更新。看個例子:
<ListItem
v-for="item in list"
:id="item.id"
:active-id="activeId" />
我們切換列表項的 activeId 時,所有的 ListItem 組件都發生了更新,明顯不合理。修改為:
<ListItem
v-for="item in list"
:id="item.id"
:active="item.id === activeId" />
將對比邏輯放入到父組件中實現,active 這個 prop 每次修改,只會更新兩個子組件。
5.2.2 v-once(元素和組件之渲染一次,跳過更新)
v-once 僅渲染元素和組件一次,並跳過之後的更新。
在隨後的重新渲染,元素/組件及其所有子項將被當作靜態內容並跳過渲染。這可以用來優化更新時的性能。
<!-- 單個元素 -->
<span v-once>This will never change: {{msg}}</span>
<!-- 帶有子元素的元素 -->
<div v-once>
<h1>Comment</h1>
<p>{{msg}}</p>
</div>
<!-- 組件 -->
<MyComponent v-once :comment="msg" />
<!-- `v-for` 指令 -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
5.2.3 v-meno(有條件的跳過更新,3.2 以上版本可用)
v-meno 緩存一個模板的子樹。用來有條件地跳過某些大型子樹或者 v-for 列表的更新。
在元素和組件上都可以使用。為了實現緩存,該指令需要傳入一個固定長度的依賴值數組進行比較。如果數組裏的每個值都與最後一次的渲染相同,那麼整個子樹的更新將被跳過。比如:
<div v-memo="[valueA, valueB]">
...
</div>
如果 v-memo="[]",效果就和 v-once 一致。
5.2.4 計算屬性穩定性(返回對象時對比具體屬性,再決定是否改變)
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)
watchEffect(() => console.log(isEven.value)) // true
// 這將不會觸發新的輸出,因為計算屬性的值依然為 `true`
count.value = 2
count.value = 4
上面代碼中,只有 isEven 計算屬性的返回值發生改變,才會導致 watchEffect 副作用的發生。
但是,如果前一個計算屬性返回的是對象,則每次產生的都是一個新的引用值,會導致 watchEffect 副作用每次都會發生:
const computedObj = computed(() => {
return {
isEven: count.value % 2 === 0
}
})
但是我們可以通過手動對比新舊屬性值的方式進行優化:
const computedObj = computed((oldValue) => {
const newValue = {
isEven: count.value % 2 === 0
}
if (oldValue && oldValue.isEven === newValue.isEven) {
return oldValue
}
return newValue
})
5.3 通用優化
5.3.1 v-lazy(圖片懶加載)
<img v-lazy="imgUrl" >
5.3.2 大型虛擬列表(只渲染視口部分)
成千上萬個列表項的列表會需要大量的 DOM,渲染全部會導致頁面卡頓。
在大多數場景中,用户的屏幕尺寸只會展示這個巨大列表中的一小部分。我們可以通過列表虛擬化來提升性能,這項技術使我們只需要渲染用户視口中能看到的部分。
可使用的庫: (1)vue-virtual-scroller (2)vue-virtual-scroll-grid (3)vueuc/VVirtualList (4)Element Plus 的 Virtualized Table 虛擬化表格、Tree V2 虛擬化樹形控件 和 Virtualized Select 虛擬化選擇器
5.3.3 減少大型不可變數據的響應性開銷(使用 shallowRef() 和 shallowReactive())
使用 shallowRef() 和 shallowReactive() 來繞開深度響應,將所有深層級對象視為不可變的,並且只能通過替換整個根狀態來觸發更新:
const shallowArray = shallowRef([
/* 巨大的列表,裏面包含深層的對象 */
])
// 這不會觸發更新...
shallowArray.value.push(newObject)
// 這才會觸發更新
shallowArray.value = [...shallowArray.value, newObject]
// 這不會觸發更新...
shallowArray.value[0].foo = 1
// 這才會觸發更新
shallowArray.value = [
{
...shallowArray.value[0],
foo: 1
},
...shallowArray.value.slice(1)
]
5.3.4 避免不必要的組件抽象
有些時候我們會去創建無渲染組件或高階組件 (用來渲染具有額外 props 的其他組件) 來實現更好的抽象或代碼組織。雖然這並沒有什麼問題,但請記住,組件實例比普通 DOM 節點要昂貴得多,而且為了邏輯抽象創建太多組件實例將會導致性能損失。
需要提醒的是,只減少幾個組件實例對於性能不會有明顯的改善,所以如果一個用於抽象的組件在應用中只會渲染幾次,就不用操心去優化它了。考慮這種優化的最佳場景還是在大型列表中。想象一下一個有 100 項的列表,每項的組件都包含許多子組件。在這裏去掉一個不必要的組件抽象,可能會減少數百個組件實例的無謂性能消耗。
6. 題外話
當然,性能優化是一個很大的話題,遠遠不是我們這篇文章所能夠一下概括的。如果大家感興趣,並且我後續有時間的話,可以專門就這一話題,再重新開一個專欄,和大家一起學習,深入探討性能優化的各種基礎知識和可能性。