我们做了一款单页应用形式的游戏,到了请求域的时候内存占用爆表了。虽然游戏大赛已经结束了,但是我依旧不能释怀。这个问题一直困扰着我。问题出在 Vue.js 吗?是 Netlify 吗?还是因为我们的代码有缺陷?我必须找出答案。
本文经原作者授权,由 InfoQ 中文站翻译并分享。
我内心深处对游戏的热爱,让我一直渴望能自己制作一些电子游戏。几个月前我开始将这种梦想变为现实,并第一次参加了全球游戏大赛(Global Game Jam)。我和我的团队使用 Vue.js 构建了一个名为“ZeroDaysLeft”的游戏,其形式是 Web 端的单页面应用程序。这款游戏的主题是环境保护,我们考虑到商业活动对地球环境的影响,希望就这个话题做一些有益的探讨。使用 Vue.js 制作的游戏并不多。我的团队迟到了一天,然后用猜拳的方式选择了我们要用的框架;我们飞快地写完了代码,并在周末结束时做出了游戏的可运行版本。在本地测试时一切都很顺利。自然,我们为自己第一次写出来的游戏作品感到自豪,并希望与世界分享它。
可是问题出现了——当我们构建好应用并开始查询域时,内存占用爆表了。它几乎没法正常运行,不管换什么机器都会卡住不动,即使在强大的基于 Intel i7 处理器的系统上程序也会崩溃。游戏大赛的时间限制把我们拉回了现实,我们决定搁置生产性能问题,这样起码我们能做出一款能在自己的设备上运行的完整游戏。就像大部分的“已完成”项目一样,第二天我们就把它抛在脑后了。
但我自己没法释怀。它一直困扰着我。问题是出在 Vue.js 上吗?是 Netlify 吗?还是因为我们的取巧代码?我必须找出答案。
调查性能下降的原因
我首先使用Lighthouse进行了快速测试。所幸 Firefox 为此提供了一个浏览器插件。下面就是我得到的结果。
89%的数字挺不错的。实际上,与许多流行的网站相比,这个表现相当出色。这个测试指出了一些潜在问题,例如速度指数和第一次有意义且有内容的绘制步骤等。从理论上讲,解决这些问题会进一步提高分数,但不一定能解决应用面临的严重性能问题。
我们的游戏中有一些图像和音频素材资源,但是两者都不至于让游戏卡死在那里。我们也可以对这些已经优化过的资源再过度优化一遍,但这可能根本就无济于事。
这个测试无法让我们真正找出可能导致这一性能问题的原因。于是我开始想:“该不会是 Vue 的问题吧?”这种想法会冒出来也没什么理由,但要是不检查一下就是蠢了。我检查了已部署站点的控制台,结果空白一片。但警告往往不会在生产中显示。当我在本地进行相同操作时,一堆 Vue 警告让我吃了一惊。
像大多数开发人员一样,我对控制台警告没那么在意,觉得它们只是警告,而不代表错误;所以我一般会把注意力集中在其他地方。或许消除这些警告可以解决我的生产问题,我决定深入研究每个问题并修复它们。
所有这些警告均来自我创建的、用来显示名为 Cards.vue 选项的组件,因此这个组件可能需要大量重写。
我决定按顺序解决这些控制台警告。
Vue.js 有很多指令,让我们能更直观地使用框架,比如说 v-for 就可以快速将数组渲染为列表。使用它时,我们需要一个 :key 才能有效地重渲染组件。但我们将一个对象用作了一个键,这是非原始值,因此导致了这个错误。我决定将 index.description 用作一个新键,因为它是一个字符串,并且在值发生更改时可以更好地重新渲染。
将 :key 更改为一个字符串(index.description)来解决上一个错误,就能解决这个重复键的错误。我们只能将字符串类型写入 DOM,因此当我们传递一个要渲染的对象时,该对象将转换为等效的字符串(即[object Object]);并且因为这以前是我们的键,所以每个对象都将转换为[object Object](除非对象有不同的值),进而会出现重复键警告。现在既然键不是对象,警告就会消失,效率也会提升。
就一个非常模糊的警告来说,这个警告似乎是最重要的:无限循环意味着内存消耗。这条消息并没有告诉我们可能出了什么问题,但它确实暗示了问题与组件中的 render 函数有关。也许是因为我们写的代码比较取巧,因此触发了不间断的更新,并占用了大量的计算能力,以至于使浏览器和设备崩溃。
这条警告至少告诉我们要检查 Cards.vue,所以我的第一个想法是检查组件中的反应属性,因为这可能会导致错误。反应属性在更改后会触发重新渲染。
我们正在显示 index.days 和 index.description 中的数据。但我们不会更改这些数据,我们从 cardInfo 数组获得 index。
我们使用这段代码对数组中的元素进行随机排序,然后将前四个元素显示为玩家选择的选项。当用户单击一个选项时将调用 effects()函数,它除了会计算一个动作如何影响游戏状态外,还使用 cardInfo 上的拼接原型删除前四个元素。
在 Vue 这种使用虚拟 DOM 的框架里,用上诸如 cardInfo 之类的反应属性后,每当数据属性的值更改时都会触发重新渲染。在我们的应用里,我们会直接使用 sort()原型来更改它,然后删除元素来重新排序。所有这些都会触发“无限”的重新渲染,从而引发警告。
我决定更改数据过滤的逻辑,并停止对反应属性 cardInfo 的多次更改。我安装了 lodash.shuffle 并定义了一个计算属性 shuffledList(),它将创建一个名为 list 的 cardInfo 副本。我对其应用了随机排序操作,并返回了一个“frozen”结果,然后拆分开来显示四张卡片。我们使用了 Object.freeze(),它将使我们返回的对象不可变,从而完全停止了所有重新渲染操作。
至此,问题解决了。
掉进框架的坑
老实说,当我刚开始调查性能下降原因的时候,还觉得我肯定要优化很多资源才能解决问题。最后这个结果说明,在使用许多框架抽象时我们都必须非常小心——特别是在 Vue 中更是如此,只有在必要时才使用某条指令,而且用法一定不能出错,因为它们绝对有自己的代价。
这还让我开始思考自己做过的其他工作,其中应用程序可能会因为框架而出现不必要的性能问题。大多数现代的前端框架都有很多抽象,使我们能更轻松地为 Web 制作应用程序。但我们应该牢记一点,那就是使用这些东西可能会引发潜在的性能问题。
我经常使用 Vue.js,所以决定探索一些我以前用过的指令,以前我用这些指令的时候完全没考虑过它们可能对应用程序带来的性能影响。其中有三条非常流行的指令进入了我的视线。
v-if 和 v-show
这两条指令都是用来有条件地渲染元素的,但是它们背后的工作机制却大不相同,因此用法也大相径庭。v-if 一开始不会渲染组件,而只在条件为真时才渲染组件。这意味着当你多次切换组件的可见性时,就会不断重新渲染。如果你要多次更改组件的可见性,那就不要使用这个功能。这会影响你的性能。
v-show 是一个很好的替代品。不管你是否启用 CSS 都会渲染你的组件,但是只会根据条件是 true 还是 false 来决定组件是否可见。这种方法确实有其缺点,因为它不会将非必要组件的渲染推迟到你需要它们在屏幕上实际出现的时候。如果你的初始渲染没那么复杂,那么它就很合适。
v-for
这条指令通常用来从数组中渲染列表。它有一个特殊的语法,形式为 item in list,其中 list 是源数据数组,而 item 是要迭代的数组元素的别名。默认情况下,Vue 在源数据数组上添加 watchers,每当发生更改时它就会触发重新渲染。这种持续的重新渲染可能会对应用程序性能产生不利影响。如果你只想可视化对象,那么 Object.freeze()是一个很好的解决方案,可以大大提高性能。但是请务必记住,你将无法更新组件或编辑对象数据。
在这个研究过程中我还意识到,Lighthouse 可能检查的是以更直接的方式影响用户体验的应用性能指标,所以接下来我的疑问就是如何跟踪服务器上的应用程序性能。
我们是不是太依赖直觉,是不是在假设开发人员知道自己在做什么,假设他们遵循的是最佳实践?不管怎样,这次经历让我对单页应用程序的性能产生了不同的看法。大家可以在 GitHub 上查看上述项目的存储库,也欢迎大家在Twitter上和我打招呼。
原文链接:
https://stackoverflow.blog/2020/03/25/tracking-down-performance-pitfalls-in-vue-js/
评论 6 条评论