天冷了,加衣服 ~
背景
最近公司级重要项目 CRM 收到坐席反馈说策略向导页面卡顿,严重影响了坐席的使用率和满意度,但曾经开发这块功能的同事早已经离职的离职,剩下的也不再负责这一块的业务,于是老大交给了我,让我务必尽快解决!
也算是一次临危受命了 ~
策略向导页面展示方式类似于脑图,用于帮助坐席在短时间内找到最精准的策略以响应用户需求。
一个策略大概长下面这样:
策略界面可以展示多个脑图,并通过 Tab 页签进行切换。
问题最直观的表现就是切换 Tab 页签尤其卡顿!
排查
按照坐席反馈的复现步骤,点击页面上的 【获取更多权限】 按钮将会展开所有节点,当展开之后来回切换脑图即可复现,轻则需要等待五六秒才能切换成功,重则直接将页面卡崩掉。复现之后,通过 Chrome DevTool 中的 Performance 面板捕捉切换的过程:
可以清晰的看到,当点击切换之后,较多的时间耗在了 show
方法上
展开具体的 Call Tree 看看
当我们展开后发现 init_nodes_size
中触发的 Layout
耗时较长,达到了 2919.8 ms
,大概占用了整个时间的 65.2%
。定位到代码中如下:
上面是 init_nodes_size
的具体代码,第一步中获取了元素的 view_data.element
的 clientWidth
和 clientHeight
,然后在第二步中设置了元素 view_data.callout
的 width
并且还在第三步再次获取了 view_data.callout
的 clientHeight
,这一个函数就触发了两次重排,如果该函数在整个更新逻辑只执行一次倒不会有太明显的性能问题,但是通过调用栈向上查看,发现是 init_nodes
内部调用了 init_nodes_size
,并且在同一时间内会循环每个节点执行一次 init_node_size
,由于实际生产环境中节点数量非常大,因此该函数将会导致严重的性能下滑。
修复
既然找到了问题的根本原因,那就很好解决了。针对这里的问题,我对代码做了如下调整:
由于之前的代码在 init_nodes_size
最后还再次获取了 view_data.callout
的 clientHeight
,但在获取之前并没有导致 clientHeight
变化的操作,因此将它提前,放到了上图第一个红框中,这样一来如果需要获取 view_data.callout
的 clientHeight
则会和上面获取 view_data.element
的宽高的代码一并执行,中途不会触发重排。
另外,在第二个红框中,我将之前修改 css 的代码封装到了临时的函数 up
(update
的缩写)中并挂载到 node
上,这部分代码在执行完所有节点的 init_nodes_size
之后的第三个红框中批量触发,从而只触发一次重排即可完成整个脑图节点的更新。考虑到 up
函数实际上只会执行一次,当执行完成之后通过 delete
将它释放,从而避免潜在的内存溢出风险。
如果追求更加苛刻的性能体验,可以将需要修改的参数直接挂载到 node
上,然后在批量更新的时候直接修改对应元素节点的属性,从而可以节省执行 up
函数时调用栈切换的时间,但可读性不如目前的这种方式,并且在我看来那属于过度优化,毕竟即便是几千个函数上下文的切换按照 js 的执行速度几乎也是瞬间完成的,但这个思路很好,它来源于很早以前无意中看到的问题 for 循环和 forEach 哪个更快?
,由于 forEach
会频繁切换上下文导致部分时间开销,因此相同的操作肯定是 for
更快。
经过本次优化,我们再看看 Performance 面板:
如上图所示,优化之后,Layout
出现在 init_nodes
下,而 init_nodes_size
下不再存在 Layout
,并且时间大幅缩减,从之前耗时的 2919.8 ms
减少到了 211.4 ms
,耗时减少了约 93%
。
结语
以前对 重绘
和 重排
对性能造成的影响一直只停留在理论层面,并没有实际深刻的体会,但这次让我认识到频繁的重排对性能是毁灭性的!
但好在经过对症下药之后,现在的策略切换已经显得非常流畅,但我总隐约感觉策略这里还有不少历史遗留的隐藏问题需要解决...
先告一段落了,不过不得不说,小小的改动能大大的提升了整个页面的性能,心中油然升起一种成就感满满的感觉!
疫情当前,大家加油!