前言
在前端开发中,性能优化是一个至关重要的环节。Chrome 浏览器的 火焰图
工具可以帮助我们发现性能瓶颈。本文将介绍如何利用 Chrome 浏览器的火焰图来优化前端项目性能,通过实际案例分析,了解如何找到性能瓶颈并进行优化。
业务背景
我们有一个前端页面,类似于树组件的渲染,其数据结构如下:
ts
interface ComponentMetaProps {
name: string;
description?: string;
defaultValue: string | Array<any> | object; // 当前节点属性值
title: string;
children: Array<ComponentMetaProps>; // 子节点
extraProps?: any;
condition?: 'display' | 'hidden' | 'none'; // 当前组件设置项是否展示
display?: 'display' | 'hidden'; // 当前组件是否展示
parentPath: string; // 父节点路径
childrenPath: string; // 子节点路径
...
}
渲染效果如下,就像一个俄罗斯套娃,无限嵌套
技术需求:设计一段逻辑,对这份数据进行增改操作,需要处理的字段有 display,parentPath,childrenPath,defaultValue 等。
准备工作
我们需要了解如何启用火焰图。具体步骤如下:
- 打开 Chrome 浏览器的开发者工具,找到 Performance 标签页。
- 点击录制按钮开始记录性能数据。
- 在前端页面上触发目标交互。
- 点击停止按钮结束录制,火焰图将显示在 Performance 标签页中。
在此,我们需要关注一个关键点:即便已经查看了火焰图,如果无法从中找到自己编写的方法,那么就无法进行进一步的优化。为了解决这个问题,我分享一下我在火焰图中定位目标方法的技巧:
- 在开始录制前,一定先刷新浏览器。
- 在页面上仅操作自己编写的交互,避免进行过多其他操作。
- 观察火焰图上的 CPU 项,根据波峰出现的位置找到对应的方法所在的火焰(交互动作发生时,CPU 项必定会出现波峰)

初始技术实现
处理树结构,我的的第一个操作就是递归遍历数据,然后根据特征字段做 if..else.. 判断处理,增改随心所欲,简单易实施
ts
/** 具体的实现代码不便放在这里 */
// 递归遍历 伪代码
const recursive = (props) => {
return props.map(node => {
// 打印当前节点 console.log(node);
if (node.children) {
return recursive(node.children);
}
return node;
})
}
寻找性能瓶颈
虽说程序员要对自己写的代码有自信,我自信这初始的技术实现是有问题的
初步实现了技术需求后,我想要测试一下自己写的代码性能怎么样?我们需要在火焰图中找到调用的方法。通过分析方法耗时,我们可以找到性能问题点

我们放大时间线,可以更清楚地看到每个方法的 耗时
和 调用次数

这样就可以清楚地看到哪些方法在拖慢程序的性能。这里可以看到总耗时在 67.15 ms
优化措施
分析
从火焰图中,我们可以看到存在很多次递归方法的调用,这是由于我们的递归算法导致,那就从两方面来考虑:
- 能否替换性能更好的算法?
- 能否减少遍历的次数?
第一轮优化
- 将递归算法替换为
深度优先遍历
。深度优先遍历通常在处理大量数据时具有更好的的性能表现
ts
/** 具体的实现代码不便放在这里 */
// 深度优先遍历 伪代码
const depthFirst = (data) => {
// 深拷贝原数据
const stack = cloneDeep(data)
// 模拟栈,管理结点
while (stack.length) {
// 栈顶结点出栈
const node = stack.shift();
// 打印当前节点
console.log(node);
let subProps = node.children || [];
// 子节点有值
if (subProps?.length) {
// 将候选顶点入栈,进行下一次循环
stack.unshift(...subProps.flat());
}
}
};
- 优化数据结构,根据业务特性,对一些业务上被隐藏的节点不做纳入遍历的范围(本案例中主要是判断 condition、display 这两个属性)

这两步做完后,我们的总耗时下降到了 6.02 ms
,但作为具有工匠精神的前端程序员,能不能对这段逻辑再进一步的优化?
第二轮细节优化
解决了主要问题点后,我们看下还有哪些小的耗时可以被优化。我们在放大火焰图,查看具体方法的耗时,发现第三方库 lodash.cloneDeep
深拷贝操作耗时有 0.38ms
。如果是只调用一次,那肯定是没有什么问题,但是在我们这里的遍历中,会有很多次的调用,累计的耗时就会很长
使用JSON.parse(JSON.stringify())
替换 lodash 的深拷贝操作。这种方法虽然可能在某些情况下存在局限性,但在大多数场景下性能更优

通过火焰图不仅可以查看到 lodash.cloneDeep 的耗时,还可以查看到其他方法的耗时,比如在本案例中,其实还优化
lodash.get
方法
ts
// 根据业务特性,重写 lodash.get 方法
// paths 是当前节点在整棵树种的路径,例如:a.b.c => ['a', 'b', 'c']
const getValue = (source: any, paths: Array<string>, defaultValue = undefined) => {
let result = source;
for (const p of paths) {
result = result?.[p];
}
return result === undefined ? defaultValue : result;
};
优化后,我们再次使用火焰图进行性能测试,发现整个交互的耗时从 67.15 ms
下降到了 3.79 ms
,看到结果时,自己也吓一跳,性能得到了很大的提升。
总结
通过此案例,我们还得到了一个结论:尽管第三方库为我们提供了便利,但由于其需要考虑众多实际情况,因此不可避免地增加了一些额外操作,从而导致整个方法的耗时增加。若我们希望实现高性能逻辑,最佳做法是在确保安全和稳定
的前提下,自行编写
相应的工具方法,而非依赖第三方库。
Chrome 浏览器的火焰图是一个非常实用的工具,可以帮助我们定位前端性能问题并进行优化。通过实际案例分析,我们可以发现优化方法的选择对性能提升具有重要影响。在进行前端性能优化时,不妨尝试参考火焰图来实施优化。