VUE 渲染效能優化
VUE 渲染效能優化
教你如何渲染 5000 個 svg
圖示不卡頓
這是無意間發現的一個課題,引起了我的興趣…
事情是這樣的,有一天我發現,專案在進入路由icon 集合
頁面時,頁面有概率出現短暫的頓挫感
像這樣
看麵包屑就知道卡了,淦
年輕人,這樣好嗎?這樣不好。
定位問題:
Icon 集合是從quasar
中匯出的源資料,一共是material Icon
1317 個 +fontawesome-v5 Icon
1601 個 = 2918 個SVG
圖示。正是因為一次渲染的svg
圖示太多,所以在進入頁面時會有頓挫感。
呵,強迫症,我就是要把它搞掂
第一次優化:
既然一次載入 3000 個圖示會引起卡頓,那麼就使用分組渲染的方式來載入圖示,之後使用v-for
渲染出分組後的中的圖示,程式碼如下:
initMaterialIcon () {
// 基於 quasar-ui 的約定,需要將 icon 的名稱轉為下劃線的形式(蛇形)
// 這一段只是單純的獲取圖示 materialIcons 下劃線格式命名集合
for (const i in materialIconsSet) {
this.materialIcons_key.push(this.toLowerLine(i))
}
// 將獲取到的 materialIcons_key 圖示名稱集合分組,其中每組 300 個,每 300 ms 渲染一組
let i = 0
this.timer1 = setInterval(() => {
this.group_md = this.group_md.concat(this.materialIcons_key.slice(i, i + 300))
i += 300
if (i > 1320) {
clearInterval(this.timer1)
}
}, 300)
}
效果如下:
發現並沒有得到很好的效果,還是會有 5 到 10 幀左右的卡頓,有不過也在意料之中。
看來渲染svg
的話沒有單純的文字那麼簡單,看了應該是和迴流 / 重繪有關了,不懂的同學可以看一下這篇文章 你真的瞭解迴流和重繪嗎
去看了一下v-for
的原始碼,使用的是createElement
來建立節點,createElement
相比createDocumentFragment
來說渲染能力差一點
:::tip
當然雖說createElement
渲染能力比較差,但是相比createDocumentFragment
它也有自己的特點,需要了解的請自行百度哦
:::
第二次優化
確定好以迴流 / 重繪為下一次的解決方向後,接下來就是使用createDocumentFragment
和requestAnimationFrame
來進行渲染操作了
:::tip
requestAnimationFrame
可以暫且把它等價為setTimeout
,不同的是requestAnimationFrame
的執行週期是根據你當前螢幕裝置的重新整理頻率來確定的,比如你的螢幕裝置的頻率是60 HZ
,那麼requestAnimationFrame
的執行週期就是 16ms,等價於 setTimeout( ()=>{ }, 16ms )
:::
因為一開始不能確定渲染一個比較大的svg
圖示需要多久,因此第一次寫程式碼時,就直接寫為
螢幕每重新整理一次渲染一個svg
圖示,程式碼如下:
initMaterialIcon () {
// 獲取圖示 materialIcons 下劃線格式命名集合
for (const i in materialIconsSet) {
this.materialIcons_key.push(this.toLowerLine(i))
}
this.$nextTick(() => {
// 渲染入口
this.RenderMDIcon(0)
})
}
RenderMDIcon (i) {
if (i >= 1317) {
cancelAnimationFrame(this.timer1)
} else {
// 建立虛擬文件碎片
const fragment = document.createDocumentFragment()
// 建立虛擬節點
const li = document.createElement('li')
// 寫入資料
li.innerText = this.materialIcons_key[i]
li.setAttribute('class', 'myIcon material-icons q-icon notranslate')
fragment.appendChild(li)
// 將虛擬文件碎片加入正式文件流
document.getElementById('mdtext').appendChild(fragment)
i++ // 步長 1
this.timer1 = requestAnimationFrame(() => {
this.RenderMDIcon(i)
})
}
},
效果如下:
可以看到螢幕每重新整理一次渲染一個svg
圖示還是很輕鬆的,接下來就是慢慢的更改單次渲染圖示數,找到單次渲染但又不卡的滑鼠數量作為步長即可。
(¬_¬) 狗血的是,第一頁有 1317 個圖示,使用這個方法一次渲染 1317 個圖示都不會卡頓…
程式碼如下:
// materialIcons 圖示集合初始化
initMaterial () {
// 獲取圖示 materialIcons 下劃線格式命名集合
for (const i in materialIconsSet) {
this.materialIcons_key.push(this.toLowerLine(i))
}
this.$nextTick(() => {
this.RenderMDIcon(0)
})
},
// 渲染圖示
RenderMDIcon (i) {
if (i >= 1317) {
cancelAnimationFrame(this.timer1)
} else {
const fragment = document.createDocumentFragment()
for (let j = i; j < i + 1317; j++) {
const li = document.createElement('li')
li.innerText = this.materialIcons_key[j]
li.setAttribute('class', 'myIcon material-icons q-icon notranslate')
li.setAttribute('onclick', 'window.copyIcon(' + "'" + this.materialIcons_key[j] + "'" + ')')
fragment.appendChild(li)
i += 1317 // 步長 1317
}
document.getElementById('mdtext').appendChild(fragment)
this.timer1 = requestAnimationFrame(() => {
this.RenderMDIcon(i)
})
}
},
效果如下:
確認過眼神,是我想要的效果
:::tip
題外話:經過測試,這個方式單次渲染超過 6000 個圖示時,才會開始卡頓。如果還想繼續載入,可以使用IntersectionObserver
這個 API 進行埋點,來載入接下來的資料。感興趣請自行百度哦。
:::
接下來說一下其中的原理
有的同學可能會認為,是因為requestAnimationFrame
的執行頻率緊貼著螢幕的重新整理頻率,因此渲染的速度是最佳的速度,所以看不出來卡頓。其實這是不正確的。
關鍵點在於createDocumentFragment
createDocumentFragment
會建立一個虛擬的文件碎片節點
它將需要渲染的操作統一集中在虛擬的文件碎片節點裡,之後再將虛擬的文件碎片節點插入真實的文件流,這樣的話就能保證只進行一次迴流操作。
渲染時如下圖所示,只進行了 1 次迴流:
(圖片來自:頁面優化,DocumentFragment物件詳解)
而如果按照普通的方式向文件流插入節點,每插入一次新節點就會執行一次迴流操作,這樣開銷非常大。
渲染時如下圖所示,就進行了 5 次迴流:
第三種方法:虛擬滾動
網路上對於虛擬滾動的介紹目前已經比較多了,不知道的請先自行百度
它有很多好處,比如僅渲染可見項,在任何給定時間點DOM樹中的節點數量最少,並且記憶體消耗保持在最低水平,不用擔心記憶體洩漏的問題等。
對於圖示的渲染我也使用過虛擬滾動,用的是 quasar 內建的虛擬滾動元件,效果如下:
我還模擬了 1w 個 icon 的資料,使用虛擬滾動,模擬使用者操作 90 s 頁面時的效能分析:
可以看到在記憶體到達一定的閾值之後會被立即釋放掉
不足之處就是每次渲染將要出現的資料時,會有些許卡頓
圖中 FPS 欄上面一段一段紅色幀就是渲染新 icon 時出現的卡頓
為什麼最後沒有使用虛擬滾動?有下面幾個因素:
-
虛擬滾動更適合於列表型的資料,即每一行為一項,這樣能更好的計算出滾動區域的高度。但是當前這一頁是一個 icon 集合頁,每個圖示作為一項,此時想實現每一行為一項,需要通過計算將 icon 根據自身寬度以及滾動區域寬度進行分行操作,比如將 15 個 icon 作為一行,之後根據行高和行數生成虛擬滾動。然而這樣做的話又需要監聽當前螢幕的變化去重新計算每一行需要多少個 icno,來完成其他的響應式操作,對於只顯示 3000 個圖示的頁面來說,我覺得開銷有點大…
-
quasar
內建的虛擬滾動元件在渲染即將出現的 icon 時,會有短暫卡頓,應該也是與迴流有關。關鍵是,卡頓這一下,我就有些受不了了 ( ̄(00) ̄) 這個才是主要原因… -
百度會發現很多寫著高效能的虛線滾動元件,如果想要自己完成一個虛擬滾動還是需要在其中下不少功夫 ( ̄(00) ̄)
最後還順便解決了一個記憶體洩漏問題
這是無意間在效能分析皮膚看到的,從圖中可以看到當前頁是一個靜態頁面,然後分析 5 s,可以看到堆疊資訊一直在那跳來跳去,不知道執行了啥 js 語句用了 9786ms 。
年輕人,一看就知道這樣不好,要講武德。
開啟Bottom-up
皮膚就能看到,有幾個指令碼一直在執行是什麼鬼。
好傢伙,馬上定位問題:
原因是封裝的 lottie 元件中新增了事件監聽,並且沒有在 lottie 被銷燬時移除該監聽
不要問我為什麼知道,因為程式碼是我寫的, ( ̄(00) ̄)
this.lottie.addEventListener('data_ready', handlerFinish)
如何解決:
在元件被銷燬的同時銷燬物件即可
beforeDestroy () {
this.lottie.destroy()
this.lottie = null
}
看一下效果,是不是爽多了:
到這裡這一次效能優化就又結束了,希望能成為你成功路上的絆腳石…
相關文章
- React渲染效能優化React優化
- 【前端效能優化】vue效能優化前端優化Vue
- 六、Android效能優化之UI卡頓分析之渲染效能優化Android優化UI
- Vue常用效能優化Vue優化
- 使用 content-visibility 優化渲染效能優化
- React 效能優化 - 避免重複渲染React優化
- Android效能優化(4):UI渲染機制以及優化Android優化UI
- vue效能優化小結Vue優化
- vue + webpack 前端效能優化VueWeb前端優化
- Vue首屏效能優化元件Vue優化元件
- Vue 應用效能優化指南Vue優化
- 前端不止:Web效能優化–關鍵渲染路徑以及優化策略前端Web優化
- vue客戶端渲染首屏優化之道Vue客戶端優化
- [譯] JavaScript 如何工作:渲染引擎和效能優化技巧JavaScript優化
- Vue首頁效能優化之gzipVue優化
- 從瀏覽器渲染原理談動畫效能優化瀏覽器動畫優化
- 小程式redux效能優化,提升三倍渲染速度Redux優化
- css 渲染優化CSS優化
- Vue 專案效能優化 — 實踐指南Vue優化
- 使用vue中後臺效能優化以及移動端優化Vue優化
- iOS 效能優化思路:介面離屏渲染、圖層混色iOS優化
- 還在為網頁渲染效能優化而苦惱嗎?網頁優化
- JavaScript 工作原理之十一-渲染引擎及效能優化小技巧JavaScript優化
- Vue-cli3.0的打包效能優化方案Vue優化
- Cookbook:優化 Vue 元件的執行時效能優化Vue元件
- VUE 9個效能優化祕密?(vue-9-perf-secrets)Vue優化
- [效能優化] 為虛擬列表增加離屏渲染和快取優化快取
- JavaScript是如何工作的:渲染引擎和優化其效能的技巧JavaScript優化
- Vue SPA專案SEO優化之預渲染Prerender-spa-pluginVue優化Plugin
- 實時渲染不是夢:通過共享記憶體優化Flutter外接紋理的渲染效能記憶體優化Flutter
- 效能優化優化
- 渲染優化之CSS Containment優化CSSAI
- 關於UI事件傳遞,影像顯示,效能優化,離屏渲染UI事件優化
- 前端效能優化:細說瀏覽器渲染的重排與重繪前端優化瀏覽器
- 瀏覽器渲染流水線解析與網頁動畫效能優化瀏覽器網頁動畫優化
- 使用非同步元件優化Vue應用程式的效能非同步元件優化Vue
- vue-cli3專案搭建配置以及效能優化Vue優化
- vue簡訊驗證效能優化寫入localstorage中Vue優化