从 67 毫秒到 3 毫秒!使用Chrome火焰图优化前端性能的实践经验

前言

在前端开发中,性能优化是一个至关重要的环节。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 等。

准备工作

我们需要了解如何启用火焰图。具体步骤如下:

  1. 打开 Chrome 浏览器的开发者工具,找到 Performance 标签页。
  2. 点击录制按钮开始记录性能数据。
  3. 在前端页面上触发目标交互。
  4. 点击停止按钮结束录制,火焰图将显示在 Performance 标签页中。

在此,我们需要关注一个关键点:即便已经查看了火焰图,如果无法从中找到自己编写的方法,那么就无法进行进一步的优化。为了解决这个问题,我分享一下我在火焰图中定位目标方法的技巧:

  1. 在开始录制前,一定先刷新浏览器。
  2. 在页面上仅操作自己编写的交互,避免进行过多其他操作。
  3. 观察火焰图上的 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

优化措施

分析

从火焰图中,我们可以看到存在很多次递归方法的调用,这是由于我们的递归算法导致,那就从两方面来考虑:

  1. 能否替换性能更好的算法?
  2. 能否减少遍历的次数?

第一轮优化

  1. 将递归算法替换为 深度优先遍历。深度优先遍历通常在处理大量数据时具有更好的的性能表现
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()); 
        } 
    } 
};
  1. 优化数据结构,根据业务特性,对一些业务上被隐藏的节点不做纳入遍历的范围(本案例中主要是判断 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 浏览器的火焰图是一个非常实用的工具,可以帮助我们定位前端性能问题并进行优化。通过实际案例分析,我们可以发现优化方法的选择对性能提升具有重要影响。在进行前端性能优化时,不妨尝试参考火焰图来实施优化。

相关推荐
BD_Marathon8 小时前
部署Spark
大数据·javascript·spark
敲代码的玉米C8 小时前
我修的那个 bug,制造了另一个 bug
前端·人工智能·架构
gis开发之家8 小时前
《Vue3 从入门到大神50篇》Vue3 源码详解(二十):生命周期钩子源码解析 —— onMounted / onUpdated 如何实现?
前端·javascript·前端框架·vue3·vue3源码
全栈项目管理程序猿8 小时前
ArcGIS JS 基础教程(9):天空盒与大气效果
javascript
Narrastory8 小时前
我用 Claude Code 写代码不到 2 小时,却花了 3 天做完这个软件—Vibe Coding 的正确姿势,是设计不是生成
前端·人工智能·github
Enaium9 小时前
我实现了KMP的SDL绑定并编写了高性能软光追
前端·kotlin
古夕9 小时前
本地正常线上白屏:一次路由切换后刷新恢复问题的排查与修复
前端·ai编程
栀鸢ouo9 小时前
解决Element Plus表格展开行横向溢出、滚动截断问题(项目实战方案)
前端·vue.js
用户39051332192889 小时前
写了5年JS,才发现这10个方法能少写一半代码
前端
平凡的阿泽9 小时前
我用TRAE Work手搓了一个「谁是卧底」小游戏
前端·javascript