前言 趁空闲时间又开了个新坑,记录下目前自己遇到过的一些前端性能优化相关经验总结 希望有朝一日我能把所有还在 draft 阶段的坑都填了 这次准备发一系列文档,预计在6月前完工。 因写内存泄漏文档时发现坑挖太大了不好填,于是这次改进为挖多个小坑一个个填 内存泄漏文档的坑也准备拆一下,不然填不动了,希望有朝一日能完工 何时应该考虑优化性能 性能优化的目标 先考虑这样的例子:玩游戏的时候掉帧,会让玩家很难受,而当开启显卡的帧生成功能时,尽管它实际上增加了输入和画面延迟,但玩家几乎感知不到,反而会感觉游戏的体验更流畅了。而帧生成做的事情,其实是将帧数提升到玩家的舒适范围内,并且保证玩家尽可能感知不到额外的输入延迟,对玩家而言就是游戏性能变得更优了。 类似地,当我们自己使用软件时,一般只有在出现明显卡顿或者延迟的时候才会感知到。 故性能优化的目标可以概括为:将延迟和卡顿降低到用户的可接受范围内。 优化的时机 先上结论:绝大部分情况下,只有出现性能瓶颈的时候才应该考虑优化性能。 性能优化的投入与回报类似一个对数曲线,越往后,付出的成本和回报就越不成正比。因此,如何平衡软件性能与开发成本通常是我们每个开发者都要面临的问题。当然,如果你是个人开发者 or 技术爱好者,那么保证自己的产品在每个细节上都做到性能优秀可能更加适合,但如果是一个全职开发者,并且公司是业务驱动而非技术驱动,那么保证自己可以快速实现功能可能是更重要的。 大部分情况下公司都是业务驱动,即使有些公司声称自己是技术驱动的,个人认为真正的技术驱动公司只会在老板不在乎利润的情况下出现,almost impossible 🙁 因此,在出现性能瓶颈,也就是用户能感知到卡顿或者延迟的时候再去考虑性能优化,是实际开发工作中的性价比之选。 常见前端性能指标 指标概览 让我们先来看一下目前在前端领域都有哪些常见的指标(留下印象即可): First Contentful Paint (FCP):首次内容绘制,衡量从网页开始加载到网页任何部分呈现在屏幕上所用的时间 Largest Contentful Paint (LCP):最大内容绘制,衡量从网页开始加载到屏幕上渲染最大的文本块或图片/视频元素所用的时间 First Input Delay (FID):首次输入延迟,衡量从用户首次与页面互动(点击链接、点按按钮或使用由 JavaScript 提供支持的自定义控件)到浏览器实际能够响应该互动的时间 Interaction to Next Paint (INP):衡量与网页进行每次点按、点击或键盘交互的延迟时间,并根据互动次数选择该网页最差的互动延迟时间(或接近最高延迟时间)作为单个代表性值,以描述网页的整体响应速度 Time to Interactive (TTI):可交互时间,衡量的是从网页开始加载到视觉呈现、其初始脚本(若有)已加载且能够快速可靠地响应用户输入的时间 Total Blocking… Continue Reading 前端性能优化之——预备篇

背景 项目用的组件库是基于Element Plus开发的(后面简称elm),但在使用过程中发现elm的tooltip组件有内存泄漏问题: 进一步排查,发现是Vue-3.3版本下,在内置的transition组件中使用v-if会有问题 定位到这个因素,是因为在element-plus的playground中,使用vue的3.3.x版本存在内存泄漏,而3.5.13版本不存在 具体排查过程就不放了,还有一篇超长内存泄漏blog正在填坑中 因早就嫌弃elm的tooltip组件使用方式不够优雅,遂把目光转向另一个大名鼎鼎的开源Vue3组件库——PrimeVue。 PrimeVue以指令的方式提供了tooltip,比使用组件优雅很多(其实博主已经把elm的tooltip二次封装为指令形式使用了) 正文 问题定位 通过对PrimeVue进行了与elm相同的内存泄漏测试(反复触发tooltip显隐),发现这货的tooltip居然也有内存泄漏问题(如下图),再次印证了草台班子理论。 一番排查后,最终定位到导致泄露的代码: 可以看出只要不触发resize事件,onWindowResize方法的闭包作用域中会一直持有对el的引用 查一查有没有相关的PR: 发现这个问题还没人解决,目测修复也比较简单,故准备给组件库提个PR 梳理逻辑 先来看看PrimeVue的tooltip都有啥 可以看到里面有跟Vue指令定义差不多的生命周期方法和一部分自己的内部方法methods,图中框起来的部分就是产生内存泄漏的主要代码 其中的BaseTooltip是PrimeVue在Vue的ObjectDirective基础上做了二次封装及自己的一些扩展的方法 顾名思义,show方法用于元素的显示,hide方法用于元素隐藏,tooltipRemoval则用于移除tooltip元素,展开看一下内部代码 tooltipActions代码: 到这里我们可以知道: 在触发指令绑定元素的mouseEnter和focus事件时,会调用show方法显示tooltip show方法会调用tooltipActions,而tooltipActions中的会创建window上的resize事件监听器,且不触发就不会被释放,进而导致el一直被该监听器方法创建的闭包作用域引用 看的过程中发现两行不对劲代码,准备顺便一起改了: 修复思路 首先,我们需要能够从外面引用到监听器,以便在其他地方销毁,先将其从tooltipActions里提出来 接下来,需要解决onWindowResize中el参数和this指向的问题,显然应该用bind完成(methods中有bindEvents方法),在bindEvents中添加: 再在tooltipRemoval和unbindEvents里加上销毁监听器的代码 测试效果 构建组件库到本地,link到本地项目测试,反复触发显隐,查看内存快照对比 问题解决,效果符合预期 单元测试,启动! 从项目根目录的package.json中可以看到组件库是有单元测试的 执行一下单元测试,确保没有产生新bug(内存泄漏的单元测试还没研究怎么写,以后整明白了再帮他们补上) 提交PR 提交代码,填写信息,提交PR表单一气呵成 并没有,好久不用Github提代码发现不能用密码登录了,又去建了个token才提上去 贴个PR链接,方便以后查(xin)看(shang) 总结 开发过程中用到window.addEventListener方法时需要谨慎,毕竟像PrimeVue这样的知名开源组件库使用它时也难免出现问题。 给开源项目做了点贡献,自己也进步了一点,感觉良好。