Cookbook组件形式:优化Vue组件的运行时性能

Vue 2.0 在发布之初,就以其优秀的运行时性能著称,你可以通过这个第三方 benchmark 来对比其他框架的性能。Vue 使用了 Virtual DOM 来进行视图渲染,当数据变化时,Vue 会对比前后两棵组件树,只将必要的更新同步到视图上。

十余年的黔西网站建设经验,针对设计、前端、开发、售后、文案、推广等六对一服务,响应快,48小时及时工作处理。成都营销网站建设的优势是能够根据用户设备显示端的尺寸不同,自动调整黔西建站的显示方式,使网站能够适用不同显示终端,在浏览器中调整网站的宽度,无论在任何一种浏览器上浏览网站,都能展现优雅布局与设计,从而大程度地提升浏览体验。成都创新互联公司从事“黔西网站设计”,“黔西网站推广”以来,每个客户项目都认真落实执行。

Vue 帮我们做了很多,但对于一些复杂场景,特别是大量的数据渲染,我们应当时刻关注应用的运行时性能。

本文仿照Vue Cookbook 组织形式,对优化 Vue 组件的运行时性能进行阐述。

基本的示例

在下面的示例中,我们开发了一个树形控件,支持基本的树形结构展示以及节点的展开与折叠。

我们定义 Tree 组件的接口如下。 data 绑定了树形控件的数据,是若干颗树组成的数组, children 表示子节点。 expanded-keys 绑定了展开的节点的 key 属性,使用 sync 修饰符来同步组件内部触发的节点展开状态的更新。



Tree 组件的实现如下,这是个稍微复杂的例子,需要花几分钟时间阅读一下。



展开或折叠节点时,我们只需更新 expanded-keysstatus 计算属性便会自动更新,保证关联子节点可见状态的正确。

一切准备就绪,为了度量 Tree 组件的运行性能,我们设定了两个指标。

初次渲染时间 节点展开 / 折叠时间

在 Tree 组件中添加代码如下,使用 console.timeconsole.timeEnd 可以输出某个操作的具体耗时。

export default {
 // ...
 methods: {
 // ...
 changeExpanded(key) {
 // ...
 this.$emit('update:expandedKeys', expandedKeys);

 console.time('expanded change');

 this.$nextTick(() => {
 console.timeEnd('expanded change');
 });
 },
 },
 beforeCreate() {
 console.time('first rendering');
 },
 mounted() {
 console.timeEnd('first rendering');
 },
};

同时,为了放大可能存在的性能问题,我们编写了一个方法来生成可控数量的节点数据。



优化后的性能耗时如下:

first rendering: 194.7890625ms
expanded change: 204.01904296875ms

你可以通过改进后的示例 (Demo2) 来观察组件的性能损耗,相比优化前有很大的提升。

双向绑定

在前面的示例中,我们使用 .syncexpanded-keys 进行了“双向绑定”,其实际上是 prop 和自定义事件的语法糖。这种方式能很方便地让 Tree 的父组件同步展开状态的更新。

但是,使用 Tree 组件时,不传 expanded-keys ,会导致节点无法展开或折叠,即使你不关心展开或折叠的操作。这里把 expanded-keys 作为外界的副作用了。


这里还存在一些性能问题,展开或折叠某一节点时,触发父组件的副作用更新 expanded-keys 。Tree 组件的 status 依赖了 expanded-keys ,会调用 this.getStatus 方法获取新的 status 。即使只是单个节点的状态改变,也会导致重新计算所有节点的状态。

我们考虑将 status 作为一个 Tree 组件的内部状态,展开或折叠某个节点时,直接对 status 进行修改。同时定义默认的展开节点 default-expanded-keysstatus 只在初始化时依赖 default-expanded-keys

export default {
 props: {
 data: Array,
 // 默认展开节点
 defaultExpandedKeys: {
 type: Array,
 default: () => [],
 },
 },
 data() {
 return {
 status: null, // status 为局部状态
 };
 },
 computed: {
 nodes() {
 return this.getNodes(this.data);
 },
 },
 watch: {
 nodes: {
 // nodes 改变时重新计算 status
 handler() {
 this.status = this.getStatus(this.nodes);
 },
 // 初始化 status
 immediate: true,
 },
 // defaultExpandedKeys 改变时重新计算 status
 defaultExpandedKeys() {
 this.status = this.getStatus(this.nodes);
 },
 },
 methods: {
 getNodes(data, level = 0, parent = null) {
 // ...
 },
 getStatus(nodes) {
 // ...
 },
 // 展开或折叠节点时直接修改 status,并通知父组件
 changeExpanded(key) {
 console.time('expanded change');

 const node = this.nodes.find(n => n.key === key); // 找到该节点
 const newExpanded = !this.status[key].expanded; // 新的展开状态
 
 // 递归该节点的后代节点,更新 status
 const updateVisible = (n, visible) => {
 n.children.forEach((child) => {
  this.status[child.key].visible = visible && this.status[n.key].expanded;
  if (child.children) updateVisible(child, visible);
 });
 };

 this.status[key].expanded = newExpanded;

 updateVisible(node, newExpanded);

 // 触发节点展开状态改变事件
 this.$emit('expanded-change', node, newExpanded, this.nodes.filter(n => this.status[n.key].expanded));

 this.$nextTick(() => {
 console.timeEnd('expanded change');
 });
 },
 },
 beforeCreate() {
 console.time('first rendering');
 },
 mounted() {
 console.timeEnd('first rendering');
 },
};

使用 Tree 组件时,即使不传 default-expanded-keys ,节点也能正常地展开或收起。






优化后的性能耗时如下。

first rendering: 91.48193359375ms
expanded change: 20.4287109375ms

你可以通过改进后的示例 (Demo3) 来观察组件的性能损耗。

冻结数据

到此为止,Tree 组件的性能问题已经不是很明显了。为了进一步扩大性能问题,查找优化空间。我们把节点数量增加到 10000 个。

// 生成 10000 个节点
this.getRandomData(4, 1000)

这里,我们故意制造一个可能存在性能问题的改动。虽然这不是必须的,当它能帮助我们了解接下来所要介绍的问题。

将计算属性 nodes 修改为在 datawatcher 中去获取 nodes 的值。

export default {
 // ...
 watch: {
 data: {
 handler() {
 this.nodes = this.getNodes(this.data);
 this.status = this.getStatus(this.nodes);
 },
 immediate: true,
 },
 // ...
 },
 // ...
};

这种修改对于实现的功能是没有影响的,那么性能情况如何呢。

first rendering: 490.119140625ms
expanded change: 183.94189453125ms

使用 Performance 工具尝试查找性能瓶颈。

Cookbook组件形式:优化 Vue 组件的运行时性能

我们发现,在 getNodes 方法调用之后,有一段耗时很长的 proxySetter 。这是 Vue 在为 nodes 属性添加响应式,让 Vue 能够追踪依赖的变化。 getStatus 同理。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。

对象越复杂,层级越深,这个过程消耗的时间越长。当我们存在 1w 个节点时, proxySetter 的时间就会非常长了。

这里存在一个问题,我们不会对 nodes 某个具体的属性做修改,而是每当 data 变化时重新去计算一次。因此,这里为 nodes 添加的响应式是无用的。那么怎么把不需要的 proxySetter 去掉呢?一种方法是将 nodes 改回计算属性,一般情况下计算属性没有赋值行为。另一种方法就是冻结数据。

使用 Object.freeze() 来冻结数据,这会阻止修改现有的属性,也意味着响应系统无法再追踪变化。

this.nodes = Object.freeze(this.getNodes(this.data));

查看 Performance 工具, getNodes 方法后已经没有 proxySetter 了。

Cookbook组件形式:优化 Vue 组件的运行时性能

性能指标如下,对于初次渲染的提升还是很可观的。

first rendering: 312.22998046875ms
expanded change: 179.59326171875ms

你可以通过改进后的示例 (Demo4) 来观察组件的性能损耗。

那我们能否用同样的办法优化 status 的跟踪呢?答案是否定的,因为我们需要去更新 status 中的属性值 ( changeExpanded )。因此,这种优化只适用于其属性不会被更新,只会更新整个对象的数据。且对于结构越复杂、层级越深的数据,优化效果越明显。

替代方案

我们看到,示例中不管是节点的渲染还是数据的计算,都存在大量的循环或递归。对于这种大量数据的问题,除了上述提到的针对 Vue 的优化外,我们还可以从减少每次循环的耗时和减少循环次数两个方面进行优化。

例如,可以使用字典来优化数据查找。

// 生成 defaultExpandedKeys 的 Map 对象
const expandedKeysMap = this.defaultExpandedKeys.reduce((map, key) => {
 map[key] = true;
 return map;
}, {});

// 查找时
if (expandedKeysMap[key]) {
 // do something
}

defaultExpandedKeys.includes 的事件复杂度是 O(n), expandedKeysMap[key] 的时间复杂度是 O(1)。

更多关于优化 Vue 应用性能可以查看Vue 应用性能优化指南。

这样做的价值

应用性能对于用户体验的提升是非常重要的,也往往是容易被忽视的。试想一下,一个在某台设备运行良好的应用,到了另一台配置较差的设备上导致用户浏览器崩溃了,这一定是一个不好的体验。又或者你的应用在常规数据下正常运行,却在大数据量下需要相当长的等待时间,也许你就因此错失了一部分用户。

总结

性能优化是一个长久不衰的话题,没有一种通用的办法能够解决所有的性能问题。性能优化是可以持续不端地进行下去的,但随着问题的深入,性能瓶颈会越来越不明显,优化也越困难。

本文的示例具有一定的特殊性,但它为我们指引了性能优化的方法论。

  • 确定衡量运行时性能的指标
  • 确定优化目标,例如实现 1W+ 数据的秒出
  • 使用工具(Chrome Performance)分析性能问题
  • 优先解决问题的大头(瓶颈)
  • 重复 3 4 步直到实现目标

以上所述是小编给大家介绍的Cookbook组件形式:优化 Vue 组件的运行时性能,希望对大家有所帮助,如果大家有任何疑问欢迎给我留言,小编会及时回复大家的!


分享名称:Cookbook组件形式:优化Vue组件的运行时性能
本文网址:http://pwwzsj.com/article/pggsed.html