记一次卡顿的性能优化经历实操
本篇的性能優(yōu)化不是八股文類(lèi)的優(yōu)化方案,而是針對(duì)具體場(chǎng)景,具體分析,從排查卡頓根因到一步步尋找解決方案,甚至是規(guī)避等方案來(lái)最終解決性能問(wèn)題的經(jīng)歷實(shí)操
所以,解決方案可能不通用,不適用于你的場(chǎng)景,但這個(gè)解決過(guò)程是如何一步步去處理的,解決思路是怎么樣的,應(yīng)該還是可以提供一些參考、借鑒意義的
當(dāng)然,也許你還有更好的解決方案,也歡迎評(píng)論教一下,萬(wàn)分感謝
問(wèn)題現(xiàn)象
我基于 twaver.js 庫(kù)實(shí)現(xiàn)了一個(gè)園區(qū)內(nèi)網(wǎng)絡(luò)設(shè)備的拓?fù)涑尸F(xiàn),連線表示設(shè)備間的拓?fù)潢P(guān)系,線路上支持流動(dòng)動(dòng)畫(huà)、告警動(dòng)畫(huà)、鏈路信息等呈現(xiàn),如:
但當(dāng)呈現(xiàn)的節(jié)點(diǎn)數(shù)量超過(guò) 1000 后,動(dòng)畫(huà)開(kāi)始有點(diǎn)丟幀,操作有點(diǎn)點(diǎn)滯后感
超過(guò) 5000 個(gè)節(jié)點(diǎn)后,頁(yè)面就非常的卡頓,難以操作
所以,就開(kāi)始了性能優(yōu)化之路
猜測(cè)&驗(yàn)證
猜測(cè) 1:Vue 框架的響應(yīng)式處理導(dǎo)致的性能瓶頸
之所以有這個(gè)猜測(cè)是因?yàn)椋以诠俜浇o的 demo 上體驗(yàn)時(shí),上萬(wàn)個(gè)節(jié)點(diǎn)時(shí)都不卡頓,更何況是一千個(gè)節(jié)點(diǎn)而已
而我的項(xiàng)目跟官方 demo 的差異有兩塊:
- 我用 vue 框架開(kāi)發(fā),官方 demo 用的純 html + js
- 我功能已經(jīng)開(kāi)發(fā)完,所以實(shí)際上還參雜了其他各種實(shí)現(xiàn)的代碼,官方 demo 很簡(jiǎn)單的純節(jié)點(diǎn)和鏈路
為了驗(yàn)證這個(gè)猜想,我另外搞了個(gè)空項(xiàng)目,純粹就只是把官方 demo 的代碼遷移到 vue 上運(yùn)行起來(lái)而已,如:
【10000 個(gè)節(jié)點(diǎn),20000 條連線,twaver 官方 demo 耗時(shí) 250ms,不卡頓】
【10000 個(gè)節(jié)點(diǎn),20000 條連線,vue 實(shí)現(xiàn)的 demo 耗時(shí) 11500ms,操作上有 0.5s 的滯后感】
同樣的代碼,同樣的數(shù)據(jù)量,區(qū)別僅僅是一個(gè)用純 js 實(shí)現(xiàn),一個(gè)用 vue 實(shí)現(xiàn),但兩邊的耗時(shí)差異將近 45 倍
所以就開(kāi)始思考了,Vue 框架能影響到性能問(wèn)題的是什么?
無(wú)非不就是響應(yīng)式處理,內(nèi)部會(huì)自動(dòng)對(duì)復(fù)雜對(duì)象深度遍歷去配置 setter, getter 來(lái)攔截對(duì)象屬性的讀寫(xiě)
而 twaver 的對(duì)象結(jié)構(gòu)又非常復(fù)雜,就導(dǎo)致了一堆無(wú)效的響應(yīng)式處理耗時(shí)資源:
看到?jīng)]有,twaver 的兩個(gè)變量 box 和 network,內(nèi)部結(jié)構(gòu)非常復(fù)雜,N 多的內(nèi)嵌對(duì)象,全部都被響應(yīng)式處理,這占用的資源是非常恐怖的
(注:Vue2.x 版本可以直接在開(kāi)發(fā)者工具面板上查看對(duì)象內(nèi)部是否有 setter 和 getter 就知道這個(gè)對(duì)象是否有被響應(yīng)式處理)
但我們其實(shí)又不需要它能夠響應(yīng)式,我們只是想使用 twaver 對(duì)象的一些 api 而已
那么該怎么來(lái)避免 Vue 對(duì)這些數(shù)據(jù)進(jìn)行的響應(yīng)式處理呢?
下一章節(jié)里再具體介紹解法,至少到這里已經(jīng)明確了卡頓的根因之一是 Vue 對(duì) twaver 的數(shù)據(jù)對(duì)象進(jìn)行了響應(yīng)式處理而引發(fā)的性能瓶頸
猜測(cè) 2:動(dòng)畫(huà)太多導(dǎo)致的性能瓶頸
這個(gè)應(yīng)該是顯而易見(jiàn)的根因之一了,每條鏈路上都會(huì)有各種動(dòng)畫(huà),而實(shí)現(xiàn)上又是每條鏈路內(nèi)部自己維護(hù)自己的動(dòng)畫(huà)管理器(twaver.Animate)
簡(jiǎn)單去撈了下 twaver 內(nèi)部源碼實(shí)現(xiàn),動(dòng)畫(huà)管理器用了 requestAnimationFrame 來(lái)實(shí)現(xiàn)動(dòng)畫(huà)幀,用了 setTimeout 來(lái)實(shí)現(xiàn)動(dòng)畫(huà)的延遲執(zhí)行
那么當(dāng)節(jié)點(diǎn)成千上萬(wàn)時(shí),肯定會(huì)卡頓,畢竟這么多異步任務(wù)
而之所以會(huì)這么實(shí)現(xiàn),原因之一是官方給的鏈路動(dòng)畫(huà) demo 就是這么做的,當(dāng)初做的時(shí)候直接用 demo 方案來(lái)實(shí)現(xiàn)了
而 demo 顯然只是介紹鏈路動(dòng)畫(huà)怎么實(shí)現(xiàn)而已,不會(huì)給你考慮到極端場(chǎng)景的性能瓶頸問(wèn)題
那么怎么解決呢?不難,無(wú)非就是抽離復(fù)用 + 按需刷新思路而已,具體也是下面講解
猜測(cè) 3:一次性呈現(xiàn)的節(jié)點(diǎn)鏈路太多導(dǎo)致的性能瓶頸
這也是顯而易見(jiàn)的根因之一,就像長(zhǎng)列表問(wèn)題一樣,一次性呈現(xiàn)的節(jié)點(diǎn)鏈路太多了,必然會(huì)導(dǎo)致性能瓶頸問(wèn)題
也不需要去驗(yàn)證了,思考解決方案就行
但這跟長(zhǎng)列表實(shí)現(xiàn)上有點(diǎn)不太一樣,因?yàn)?twaver 內(nèi)部是用 canvas 來(lái)繪制節(jié)點(diǎn)和鏈路的,并不是用 dom 繪制,所以虛擬列表那種思路在這里行不通
但本質(zhì)上的解決都一個(gè)樣,無(wú)非就是一次性沒(méi)必要呈現(xiàn)這么多節(jié)點(diǎn),因?yàn)橐黄羶?nèi)又顯示不了,沒(méi)有意義
所以,按照這種思路去尋找解決方案,具體也下面講講
猜測(cè) 4:dom 節(jié)點(diǎn)太多導(dǎo)致的性能瓶頸
雖然 twaver 內(nèi)部是用 canvas 繪制的節(jié)點(diǎn)和鏈路,但當(dāng)節(jié)點(diǎn)畢竟復(fù)雜時(shí),比如:
這種時(shí)候用 canvas 畫(huà)不出來(lái),只能用 div 繪制,twaver 也支持 HTMLNode 類(lèi)型節(jié)點(diǎn),這就意味著也會(huì)存在 dom 過(guò)多的場(chǎng)景
而 dom 導(dǎo)致的性能問(wèn)題包括 dom 元素過(guò)多,頻繁操作 dom
因此解決方案上就是盡量避免創(chuàng)建過(guò)多的 dom 元素以及避免頻繁操作 dom 即可,具體也下面講
解決方案
繞過(guò) Vue 的自動(dòng)對(duì)數(shù)據(jù)模型進(jìn)行的響應(yīng)式處理
Vue2.x 框架內(nèi)部會(huì)自動(dòng)將聲明在 data 里的變量進(jìn)行響應(yīng)式處理,第一個(gè)想到的是嘗試用 Object.freeze 來(lái)凍結(jié)對(duì)象,例如:
this.box = Object.freeze(new twaver.ElementBox());
但有兩個(gè)問(wèn)題:
- Object.freeze 是淺凍結(jié),不是深度凍結(jié),內(nèi)嵌的對(duì)象好像還是會(huì)被響應(yīng)式處理
- 可能會(huì)引發(fā)功能異常,因?yàn)闆](méi)法確認(rèn)三方庫(kù)內(nèi)部是否有用到對(duì)象的枚舉、遍歷、擴(kuò)展等能力
那么還有其他什么方案嗎?
如果是 Vue3.x 的話,因?yàn)轫憫?yīng)式處理是顯示調(diào)用,就沒(méi)有這些煩惱了。
至于 Vue2.x,內(nèi)部自動(dòng)進(jìn)行了響應(yīng)式處理,因此我們需要去源碼里看看有沒(méi)有什么辦法可以繞過(guò)響應(yīng)式處理。
注:下面是 Vue 2.7.16 版本的源碼
源碼里給 data 數(shù)據(jù)進(jìn)行響應(yīng)式處理是在 core/instance/state.ts#initData()
// core/instance/state.ts
function initData(vm: Component) {
let data: any = vm.$options.data;
data = vm._data = isFunction(data) ? getData(data, vm) : data || {};
// 省略判斷 data 為對(duì)象的代碼
// ...
const keys = Object.keys(data);
const props = vm.$options.props;
const methods = vm.$options.methods;
let i = keys.length;
while (i--) {
const key = keys[i];
// 省略判斷 data 的字段與 props 或 methods 是否有同名的場(chǎng)景
// ...
// 判斷變量命名是否是 _ 或 $ 為前綴
if (!isReserved(key)) {
proxy(vm, `_data`, key); // 這里是關(guān)鍵之一,把 data 里的對(duì)象掛載到外部 vue 組件上
}
}
const ob = observe(data); // 響應(yīng)式處理 data 數(shù)據(jù)
ob && ob.vmCount++;
}
上面的源碼里我省略了一些無(wú)關(guān)的代碼,然后有兩個(gè)關(guān)鍵點(diǎn),一個(gè)是通過(guò) isReserved(key) 判斷變量命名是否是以 _ 或 $ 開(kāi)頭的代理處理,另一個(gè)是 observe(data) 處理響應(yīng)式的 data 數(shù)據(jù)
第一點(diǎn)等會(huì)再講,先來(lái)看看是怎么對(duì) data 數(shù)據(jù)進(jìn)行響應(yīng)式處理的:
// core/observer/index.ts
export function observe(
value: any,
shallow?: boolean,
ssrMockReactivity?: boolean
): Observer | void {
// 如果該對(duì)象已經(jīng)響應(yīng)式處理過(guò)了,就跳過(guò),沒(méi)必要再次處理
if (value && hasOwn(value, "__ob__") && value.__ob__ instanceof Observer) {
return value.__ob__;
}
// 當(dāng)滿足下面條件時(shí),對(duì)對(duì)象進(jìn)行響應(yīng)式處理
if (
shouldObserve && // 總開(kāi)關(guān)
(ssrMockReactivity || !isServerRendering()) && // 非服務(wù)端渲染場(chǎng)景
(isArray(value) || isPlainObject(value)) && // 數(shù)組或?qū)ο? Object.isExtensible(value) && // 支持?jǐn)U展(即動(dòng)態(tài)增刪字段)
!value.__v_skip /* ReactiveFlags.SKIP */ && // 是否跳過(guò)響應(yīng)式處理
!isRef(value) && // // 是否是響應(yīng)式對(duì)象
!(value instanceof VNode) // 是否是 VNode 對(duì)象
) {
// 內(nèi)部遍歷對(duì)象的屬性,調(diào)用 defineReactive() 來(lái)對(duì)屬性進(jìn)行 setter, getter 攔截
// 而 setter 里又重新調(diào)用 observe() 處理屬性值,從而達(dá)到深度遞歸處理內(nèi)嵌對(duì)象屬性的響應(yīng)式效果
return new Observer(value, shallow, ssrMockReactivity);
}
}
所以,我們其實(shí)是有辦法來(lái)繞過(guò)響應(yīng)式處理的,比如給對(duì)象增加一個(gè)要跳過(guò)響應(yīng)式處理的標(biāo)志 __v_skip,如:
const box = new twaver.ElementBox();
box.__v_skip = true; // 這個(gè)是關(guān)鍵
this.box = box;
const network = new twaver.vector.Network(this.box);
network.__v_skip = true; // 這個(gè)是關(guān)鍵
this.network = network;
注意:__v_skip是 Vue2.7.x 版本后加入的邏輯,在 Vue2.6 及之前版本里,并沒(méi)有該邏輯,相反只有一個(gè) _isVue 標(biāo)志位判斷
有人說(shuō),不用這么麻煩,把變量命名改成 _ 為前綴,也能繞過(guò)響應(yīng)式處理,這是真的嗎?畢竟源碼里好像沒(méi)有看到相關(guān)的代碼
別急,還記得我上面介紹 initData() 源碼里的兩個(gè)關(guān)鍵點(diǎn)之一的 ``
// core/instance/state.ts
function initData(vm: Component) {
// 省略其他無(wú)關(guān)代碼
// ...
while (i--) {
// 省略其他無(wú)關(guān)代碼
// ...
// 判斷變量命名是否是 _ 或 $ 為前綴
if (!isReserved(key)) {
// 把 data 里的對(duì)象掛載到外部 vue 組件上
proxy(vm, `_data`, key);
}
}
// 省略其他無(wú)關(guān)代碼
// ...
}
這里會(huì)遍歷 data 里的各個(gè)屬性字段,然后把里面非 _ 或 $ 為前綴的變量都掛到外部 Vue 組件實(shí)例上,這樣我們代碼里才可以直接用 this.xxx 來(lái)操作這些變量
由于我們命名了 _box,_network 變量,這些以 _ 開(kāi)頭的變量就沒(méi)有被掛到 Vue 組件實(shí)例上,而后續(xù)我們代碼里使用 this._box = xxx 這樣來(lái)賦值變量,其實(shí)本質(zhì)上是動(dòng)態(tài)的往 Vue 組件實(shí)例上增加了一個(gè) _box 變量,由于 Vue2.x 不支持對(duì)動(dòng)態(tài)添加的屬性進(jìn)行響應(yīng)式處理,因此這才能達(dá)到繞過(guò)響應(yīng)式處理的效果
所以把變量命名改成 _ 為前綴,其實(shí)是誤打誤撞的剛好繞過(guò)了響應(yīng)式處理
Vue 官方文檔里其實(shí)也有解釋說(shuō)了:
Properties that start with _ or $ will not be proxied on the Vue instance because they may conflict with Vue’s internal properties and API methods. You will have to access them as vm.$data._property
大意就是,Vue 內(nèi)部變量命名就是以 _ 和 $ 為前綴命名,因此不會(huì)把 data 里以 _ 和 $ 開(kāi)頭的變量掛到外部上來(lái),防止變量命名沖突覆蓋掉內(nèi)部變量而引起異常。因此當(dāng) data 里有這些變量時(shí),使用時(shí)應(yīng)該要 this.$data._xxx 的方式來(lái)操作這些變量
雖然是誤打誤撞的繞過(guò)了響應(yīng)式處理,但這種方案不會(huì)讓代碼更繁瑣,使用上還算方便,就是需要放開(kāi) eslint 的 vue/no-reserved-keys 校驗(yàn)規(guī)則
【舉一反三】
當(dāng)用到其他一些三方庫(kù),三方庫(kù)變量又不是全局而是當(dāng)前組件內(nèi)的局部變量 data 內(nèi)部時(shí),都會(huì)存在被 Vue 響應(yīng)式處理的問(wèn)題。
如果你也有遇到這種場(chǎng)景,不防往這方面去考慮看看如果繞過(guò)響應(yīng)式處理
共同復(fù)用全局的動(dòng)畫(huà)管理器 + 按需刷新
【原實(shí)現(xiàn)方案】
每條鏈路的動(dòng)畫(huà)由各自?xún)?nèi)部實(shí)現(xiàn):
export default function FlowLink() {
FlowLink.superClass.constructor.apply(this, arguments);
this._animate = this.getAnimate();
}
twaver.Util.ext(FlowLink, twaver.Link, {
play: function (options) {
this._animate.play();
return this._animate;
},
getAnimate: function (options) {
// 內(nèi)部自己的動(dòng)畫(huà)管理器
this._animate = new twaver.Animate(
Object.assign(
{
from: 0,
to: 1,
repeat: Number.POSITIVE_INFINITY,
reverse: false,
delay: 200, // 動(dòng)畫(huà)延遲
dur: 5000, // 動(dòng)畫(huà)時(shí)才
easing: "linear", // 線性動(dòng)畫(huà)
onUpdate: (value) => {
// 更新動(dòng)畫(huà)進(jìn)度
this.setClient("anim.percent", value);
},
},
options
)
);
return this._animate;
},
});
而每條鏈路都是獨(dú)立的 FlowLink 實(shí)例對(duì)象,當(dāng)達(dá)到成千上萬(wàn)條鏈路時(shí),資源就被撐爆了,很卡
【復(fù)用全局動(dòng)畫(huà)管理器思想】
其實(shí),每條鏈路內(nèi)部的動(dòng)畫(huà)管理器是一模一樣的,那我們其實(shí)可以實(shí)現(xiàn)一個(gè)全局的統(tǒng)一動(dòng)畫(huà)管理器,這樣不管鏈路有多少條,我們的動(dòng)畫(huà)管理器都只有 1 個(gè)
但動(dòng)畫(huà)管理器就要有種途徑來(lái)找到各個(gè)鏈路,這樣才能觸發(fā)鏈路的刷新,以便它們內(nèi)部根據(jù)最新動(dòng)畫(huà)進(jìn)度來(lái)進(jìn)行渲染
【按需刷新思想】
既然動(dòng)畫(huà)管理器內(nèi)部需要撈取到鏈路來(lái)刷新,那干脆,只撈取屏幕可視范圍內(nèi)的鏈路進(jìn)行刷新,屏幕外部的鏈路就不通知刷新
這樣不就更節(jié)省性能損耗了
export default function FlowLink() {
FlowLink.superClass.constructor.apply(this, arguments);
}
twaver.Util.ext(FlowLink, twaver.Link, {
play: function () {
// 鏈路內(nèi)部不維護(hù)動(dòng)畫(huà)管理器了,只需要加個(gè)動(dòng)畫(huà)開(kāi)關(guān)即可
this.setClient("anim.enable", true);
},
});
export default class GLobalAnimation {
constructor(network) {
this._network = network; // 與動(dòng)畫(huà)關(guān)聯(lián)的拓?fù)洚?huà)布
this._linkAnimation = null; // 鏈路動(dòng)畫(huà)實(shí)例
this._linkAnimPercent = 0; // 鏈路動(dòng)畫(huà)進(jìn)度
}
playLinkAnimation() {
if (!this._linkAnimation) {
this._linkAnimation = this._initLinkAnimation();
this._linkAnimation.play();
}
}
_initLinkAnimation() {
return new twaver.Animate({
from: 0,
to: 1,
repeat: Number.POSITIVE_INFINITY,
reverse: false,
delay: 200, // 動(dòng)畫(huà)延遲
dur: 5000, // 動(dòng)畫(huà)時(shí)才
easing: "linear", // 線性動(dòng)畫(huà)
onUpdate: (value) => {
// 只重繪可視范圍內(nèi)的鏈路
try {
const state = this._network.state || {};
// 滑動(dòng)、縮放、布局過(guò)程中,都沒(méi)必要更新UI
const isReady = !state.zooming && !state.panning && !state.layouting;
if (isReady) {
// 獲取經(jīng)過(guò)縮放后的可視范圍
const viewRect = this._getZoomRect(this._network.getViewRect());
// 根據(jù)可視范圍,獲取范圍內(nèi)的鏈路對(duì)象
const nodes = this._network.getElementsAtRect(viewRect, true);
nodes.forEach((node) => {
// 刷新指定鏈路節(jié)點(diǎn)
this._network.invalidateElementUI(node, false);
});
}
} catch (error) {
console.error("[GlobalAnimation]", error);
}
},
});
}
_getZoomRect(rect) {
const zoom = this._network.getZoom() || 1;
const offset = 200;
return {
x: (rect.x - offset) / zoom,
y: (rect.y - offset) / zoom,
width: (rect.width + offset * 2) / zoom,
height: (rect.height + offset * 2) / zoom,
};
}
}
這種思路有點(diǎn)像一開(kāi)始只站在局部角度來(lái)思考代碼實(shí)現(xiàn),優(yōu)化后則是站在全局角度上來(lái)進(jìn)行的思考
而解決思路則是萬(wàn)能的復(fù)用,萬(wàn)能的懶加載,按需使用思想
交互上進(jìn)行規(guī)避,如增加默認(rèn)折疊、展開(kāi)處理
由于節(jié)點(diǎn)是直接借助 twaver 內(nèi)部的 canvas 實(shí)現(xiàn),因此節(jié)點(diǎn)數(shù)量太多導(dǎo)致的性能瓶頸問(wèn)題是 twaver 庫(kù)本身就存在的問(wèn)題,雖然 twaver 已經(jīng)做到 1W 級(jí)別的節(jié)點(diǎn)的絲滑呈現(xiàn),但當(dāng)數(shù)量繼續(xù)加上去,達(dá)到 5W,10W 級(jí)別時(shí),也會(huì)開(kāi)始出現(xiàn)操作滯后感,卡頓等性能瓶頸
也許你會(huì)說(shuō),簡(jiǎn)單,跟上個(gè)問(wèn)題一樣,按需加載不就行了,只繪制屏幕可視范圍內(nèi)的節(jié)點(diǎn),其余節(jié)點(diǎn)不繪制
理論上可行,但實(shí)現(xiàn)上難度很大
因?yàn)樯弦粋€(gè)問(wèn)題是節(jié)點(diǎn)鏈路已經(jīng)繪制完畢的基礎(chǔ)上,來(lái)進(jìn)行刷新范圍的過(guò)濾,所以只需要根據(jù)坐標(biāo)點(diǎn)信息的計(jì)算就能達(dá)到訴求
但現(xiàn)在場(chǎng)景是還沒(méi)繪制,你沒(méi)法獲知任何信息
你不知道經(jīng)過(guò)縮放、拖拽后的當(dāng)前視圖里,到底應(yīng)該呈現(xiàn)哪些節(jié)點(diǎn)
而且,twaver 是付費(fèi)框架,源碼是混淆的,你不知道內(nèi)部它是怎么實(shí)現(xiàn)的,無(wú)法參與節(jié)點(diǎn)的排版過(guò)程,也導(dǎo)致你很難下手去實(shí)現(xiàn)所謂的按需繪制問(wèn)題
再者,我們還有搜索定位的交互需求,就算你上面問(wèn)題都解決了,那當(dāng)搜索的節(jié)點(diǎn)是沒(méi)繪制的節(jié)點(diǎn),你如何去定位到該節(jié)點(diǎn)真實(shí)的位置
基于以上種種原因,考慮到投入成本的性?xún)r(jià)比,我們最終決定采用從非技術(shù)角度去優(yōu)化:從交互上進(jìn)行規(guī)避
- 增加節(jié)點(diǎn)的默認(rèn)折疊處理方案,當(dāng)超過(guò)一定數(shù)量時(shí),默認(rèn)把子孫節(jié)點(diǎn)折疊起來(lái),這樣能夠避免一次性渲染太多節(jié)點(diǎn)
- 同時(shí)增加展開(kāi)/折疊全部節(jié)點(diǎn)的快捷操作
- 由于孤點(diǎn)沒(méi)有樹(shù)形結(jié)構(gòu),因此當(dāng)超過(guò)一定數(shù)量孤點(diǎn)時(shí),需要另外處理折疊邏輯
- 搜索節(jié)點(diǎn)時(shí),發(fā)現(xiàn)節(jié)點(diǎn)處于折疊狀態(tài)的話,要自動(dòng)進(jìn)行展開(kāi)處理
簡(jiǎn)單來(lái)說(shuō)就是會(huì)設(shè)定一個(gè)閾值,當(dāng)節(jié)點(diǎn)超過(guò)這個(gè)數(shù)量時(shí),都折疊起來(lái),等用戶(hù)手動(dòng)去展開(kāi)再呈現(xiàn),相當(dāng)于分頁(yè)呈現(xiàn)的思想
dom 節(jié)點(diǎn)的懶創(chuàng)建 + 緩存和復(fù)用
有些復(fù)雜節(jié)點(diǎn)的場(chǎng)景無(wú)法用 twaver 的默認(rèn)節(jié)點(diǎn)樣式呈現(xiàn),也就用不了 canvas 實(shí)現(xiàn),只能自己用 html 方式來(lái)實(shí)現(xiàn)
但也不可能用純 html + js 實(shí)現(xiàn),還是依賴(lài)于 vue 框架,這就涉及到 vue 組件的手動(dòng)創(chuàng)建、掛載、銷(xiāo)毀
這種復(fù)雜節(jié)點(diǎn)過(guò)多時(shí),就會(huì)涉及到 dom 元素的反復(fù)創(chuàng)建、銷(xiāo)毀以及渲染過(guò)多的性能瓶頸問(wèn)題
那么解決方案上,一樣也是懶加載,但為了組件可以復(fù)用,增加了緩存和復(fù)用處理,避免相同組件要重復(fù)創(chuàng)建
具體做法則是:
- 重寫(xiě)了 twaver 繪制 dom 元素的方法邏輯,改造成懶加載方式
- 即當(dāng)節(jié)點(diǎn)不在頁(yè)面可視范圍內(nèi)的話,不掛載 dom 到界面上,避免一次性渲染太多 dom
- 收集緩存所有的 dom 組件
- 當(dāng)反復(fù)使用時(shí),直接復(fù)用緩存
- 當(dāng)銷(xiāo)毀時(shí),手動(dòng)觸發(fā) vue 的 destroy,及時(shí)銷(xiāo)毀資源
小結(jié)
其實(shí),大多數(shù)的性能問(wèn)題本質(zhì)上都是大同小異的原因:
- 無(wú)意義的內(nèi)存占用過(guò)高,如 Vue 對(duì) twaver 數(shù)據(jù)對(duì)象的響應(yīng)式處理
- 一次性處理的東西過(guò)多,如渲染上萬(wàn)個(gè)節(jié)點(diǎn)
- 短時(shí)間內(nèi)頻繁執(zhí)行某些其實(shí)沒(méi)意義的操作,如實(shí)時(shí)刷新即使在屏幕外的動(dòng)畫(huà)
- 反復(fù)創(chuàng)建、銷(xiāo)毀行為,如 dom 節(jié)點(diǎn)的反復(fù)創(chuàng)建
所以性能優(yōu)化的難點(diǎn)之一在于排查根因,找到問(wèn)題所在后,才能去著手思考對(duì)應(yīng)的解決方案
而解決思路無(wú)外乎也是大同小異:
- 按需使用、懶加載、分頁(yè)
- 緩存和復(fù)用
- 規(guī)避方法
總結(jié)
以上是生活随笔為你收集整理的记一次卡顿的性能优化经历实操的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ChatGPT的中转站(欧派API) o
- 下一篇: Javac多模块化编译