avant-propos
Je pense que les étudiants ont lu de nombreux articles sur l'optimisation des performances et que la plupart des routines ont été comprises. Par exemple, pour l'optimisation des fichiers de ressources (optimisation du packaging webpack, compression gzip, accélération cdn, mise en cache du navigateur, chargement différé, http2, etc.), l'optimisation du rendu des pages (SSR, liste virtuelle, réduction du reflow et du redessin, etc.).
Mais nous sommes plus impliqués dans les affaires, et la plupart des problèmes de performances sont causés par la négligence des développeurs. En raison de l'itération continue des fonctions métier, il a récemment été constaté que le projet présentait également des problèmes de performances similaires. Voici un problème de performances relativement caché rencontré lors du dépannage de problèmes de performances. Au cours du processus, j'ai également eu quelques idées à partager avec mon camarades de classe.
Dans cet article, vous apprendrez :
- Utilisation de l'outil Performances
- Enregistrer et analyser les graphiques de flamme
- Principe de la propriété calculée, processus cloneDeep
les problèmes de performance
Le flux de données est brièvement décrit ici. Le projet est divisé en plusieurs modules. Chaque module dépend de la QuestionList stockée dans vuex. Le module construira les données d'application et effectuera le rendu en fonction de la QuestionList.
Parlons des principaux problèmes de performances. Lors de la modification d'une certaine configuration de l'élément dans la QuestionList, il sera bloqué pendant environ 1,5 s avant la réponse. comme suit:
Après optimisation :
C'est doux comme de la soie ~ Jetons un coup d'œil à l'idée du processus d'optimisation.
Analyse de performance
Performance est un outil d'analyse visuelle fourni par Google Chrome, qui est utilisé pour enregistrer et analyser toutes les activités de l'application au moment de l'exécution, ce qui peut aider les développeurs à bien localiser les problèmes de performances.
Prêt à travailler
Afin d'éviter l'influence d'autres facteurs sur l'analyse des performances, certaines préparations doivent être effectuées :
- Activez le mode incognito dans le navigateur pour éviter l'impact sur les performances du plug-in Chrome sur la page
- Fermez le plug-in vuex logger dans l'environnement de développement du projet. Le deepCopy dans le plug-in augmentera le temps d'exécution (selon que le projet est utilisé ou non, il réduit principalement l'impact du plug-in project logger)
Comment utiliser les performances
F12 ouvre les outils de développement et sélectionne le panneau Performance :
左上角有 3 个小按钮。点击的实心圆按钮,Performance 会开始帮记录用户后续的交互操作;点击圆箭头按钮,Performance 会将页面重新加载,计算加载过程中的性能表现;点击最后一个按钮是清除记录。
因为我们是要记录用户实际操作的过程,所以选用圆按钮来记录。首先点击圆按钮开始录制,然后在页面正常进行更改配置的操作,完成后点击 stop 停止录制。
只需等待一小会,Performance 就会输出一份报告。
报告主要分为三大块,分别是:
- 概览面板:Performance 就会将几个关键指标,诸如页面帧速 (FPS)、CPU 资源消耗、网络请求流量、V8 内存使用量 (堆内存) 等,按照时间顺序做成图表的形式展现出来。
- 性能指标面板:它主要用来展现特定时间段内的多种性能指标数据,一般用的比较多的是 Main,它记录了渲染进程的主线程的任务执行记录。
- 详情面板:对应于性能面板只能得到一个大致的信息,如果想要查看这些记录的详细信息,可以通过在性能面板中选中性能指标中的任何历史数据,然后选中记录的细节信息就会展现在详情面板中了。
分析火焰图
点开 Main 指标来展开主进程的任务执行记录,为了更方便分析,可以在概览面板中拖动选择一个区间具体分析任务执行记录。
一段段横条代表执行一个个任务,长度越长,花费的时间越多;竖向代表该任务的执行记录,最上面的是主任务,继续往下就是子任务,就像函数执行栈。一般我们也称它为“火焰图”。
接下来分析火焰图,引入眼帘最长的一段任务就是 get 任务,下面的子任务依次是 updateComponent、render、compomutedGetter、evaluate等,之前看过部分 Vue 源码,大概就知道是依赖属性变化让计算属性重新求值,接着是页面渲染。但这些对于我们排查意义并不大,只能有些提示的作用,因为都是 Vue 内部执行的函数。
往下就看到 svgHeight 这个业务上的函数,这个才是优先需要去排查的点。
定位瓶颈
找到相关代码:
computed: {
questionList () {
let list = cloneDeep(this.QuestionList).map((q, qi) => {
// ...
return q
})
return list
},
svgHeight () {
let h = 0
this.questionList.forEach((q, i) => {
h += this._getQuestionHeight(i)
if (i !== this.questionList.length - 1) {
h += CONTAINER_GAP_HEIGHT
}
})
return h + CREVICE_HEIGHT
}
}
复制代码
这里可以大概定位瓶颈,在更改配置时,响应式数据变更,让计算属性(svgHeight)重新计算。
瓶颈思考
大胆假设
从上面的代码中可以看到,svgHeight 依赖另一个计算属性(questionList ),questionList 又依赖于 vuex 中的 QuestionList。
看到这里我就产生了一个疑问,计算属性只是依赖 QuestionList 这个响应式属性,并没有依赖到具体的配置字段属性,为什么在更改配置会触发重新计算?
在我百思不得其解时,突然看到 QuestionList 用了 cloneDeep 进行深拷贝。我灵光一闪,深拷贝内部不就是会去遍历属性进行拷贝的嘛。遍历属性时,触发响应式属性的 get 劫持函数,然后在内部收集了这个计算属性的依赖,这样就会过度的收集依赖,当响应式属性改变就会触发相关依赖的更新。
下面来了解下 cloneDeep 流程和计算属性是怎么收集依赖的。
cloneDeep 流程
cloneDeep.js:
function cloneDeep(value) {
return baseClone(value, CLONE_DEEP_FLAG | CLONE_SYMBOLS_FLAG);
}
复制代码
cloneDeep 方法主要调用了 baseClone。
_baseClone.js:
function baseClone(value, bitmask, customizer, key, object, stack) {
// 1. 参数处理(赋值、判断 return)
// 2. 初始化参数
// 3. 检查循环引用返回
// 4. 针对 Set 和 Map 的拷贝
var keysFunc = isFull
? (isFlat ? getAllKeysIn : getAllKeys)
: (isFlat ? keysIn : keys);
var props = isArr ? undefined : keysFunc(value);
// 5. 一般会走这里的遍历
arrayEach(props || value, function(subValue, key) {
if (props) {
key = subValue;
// value[key] 访问属性
subValue = value[key];
}
// Recursively populate clone (susceptible to call stack limits).
assignValue(result, key, baseClone(subValue, bitmask, customizer, key, value, stack));
});
return result;
}
复制代码
baseClone 内部会遍历访问属性,递归调用 baseClone,如果这里是一个响应式属性,就会触发它的 get 劫持函数去收集依赖了。
计算属性收集依赖
// 源码位置:/src/core/instance/state.js
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true }
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
复制代码
初始化计算属性配置时,会循环为每个计算属性 Watcher 创建一个“计算属性Watcher”。
// 源码位置:/src/core/observer/watcher.js
evaluate() {
this.value = this.get()
this.dirty = false
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 计算属性求值
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
popTarget()
this.cleanupDeps()
}
return value
}
复制代码
在模板渲染时会触发计算属性的 get 劫持函数,然后调用 evaluate,接着调用 get 函数。
pushTarget 将当前的“计算属性Watcher”挂到全局的 Dep.target 上。
执行 this.getter (定义计算属性时对应的回调函数),内部如果访问了响应式属性时,会触发响应式属性的 get 劫持函数,进行依赖收集。在收集依赖依赖时,就是从 Dep.target 上取的,所以我们常说的依赖也就是 Watcher。
// 源码位置:/src/core/observer/dep.js
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
复制代码
每个响应式属性会有个 subs 的数组存放 Watcher,当触发 set 劫持函数时就会执行 notify,然后循环执行 Watcher 的 update 方法。
结合回实际场景,在初始化模板渲染时 svgHeight 会执行对应的计算属性回调,然后 cloneDeep 遍历了每一个响应式属性,响应式属性收集对应的“计算属性Watcher”。后续当响应式属性更改时,就会触发计算属性更新了。
想了解更多Vue原理,可以看我之前的文章:
小心求证
当然以上只是我的猜想(我觉得八九不离十),但本着严谨的态度,还是需要做些实验来验证猜想。
如何验证呢?既然计算属性内遍历响应式属性会收集依赖,那将这步操作放到 created 中,提前拷贝好,然后计算属性再依赖这个拷贝好的数据。
data () {
return {
tempList: []
}
},
computed: {
questionList () {
let list = cloneDeep(this.tempList).map((q, qi) => {
// ...
return q
})
return list
}
},
created () {
this.tempList = cloneDeep(this.QuestionList)
}
复制代码
重新录制火焰图:
果然,没有看到 svgHeight 函数的执行,耗时也大大减少。因为对应配置字段没有收集到“计算属性Watcher”,即使配置发生更改,这里也不会再重新触发计算属性求值。那就验证了上面的猜想。
解决方案
知道瓶颈后,解决方案就简单了。其实有几种思路:
- 就像上面的做法,不在计算属性内调用 cloneDeep,而是提前拷贝好一份,以此作为计算属性的依赖。
- 但第一种的做法会有个弊端,如果我们刚好就是要依赖 QuestionList 中的某几个属性,那么可以通过 this.QuestionList[index].xxx 的方法单独访问,让需要的响应式属性去收集依赖。
但是这部分对于本篇文章来说,并不是重点,因为都是基于业务场景去解决,最重要的还是要懂得分析和定位问题。
总结
本文主要分享使用 Performance 录制火焰图并定位问题的过程,和思考问题的思路。还分析了一波 cloneDeep 流程和计算属性原理。
在此也能得出一个结论,在计算属性中使用 cloneDeep 拷贝响应式数据时,会造成依赖被过度收集,导致计算属性非预期更新的情况(如果你的业务就是要这种效果,当我没说,哈哈哈)。以上就是我在排查性能问题的全过程,希望能带给同学们一些思考和经验。