likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 10

前言

在前端开发中,性能优化是一个至关重要的环节。Chrome 浏览器的 火焰图 工具可以帮助我们发现性能瓶颈。本文将介绍如何利用 Chrome 浏览器的火焰图来优化前端项目性能,通过实际案例分析,了解如何找到性能瓶颈并进行优化。

业务背景

我们有一个前端页面,类似于树组件的渲染,其数据结构如下:

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; // 子节点路径
  ...
}

渲染效果如下,就像一个俄罗斯套娃,无限嵌套

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

技术需求:设计一段逻辑,对这份数据进行增改操作,需要处理的字段有 display,parentPath,childrenPath,defaultValue 等。

准备工作

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

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

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

  1. 在开始录制前,一定先刷新浏览器。
  2. 在页面上仅操作自己编写的交互,避免进行过多其他操作。
  3. 观察火焰图上的 CPU 项,根据波峰出现的位置找到对应的方法所在的火焰(交互动作发生时,CPU 项必定会出现波峰)

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

初始技术实现

处理树结构,我的的第一个操作就是递归遍历数据,然后根据特征字段做 if..else.. 判断处理,增改随心所欲,简单易实施

/** 具体的实现代码不便放在这里 */

// 递归遍历 伪代码
const recursive = (props) => { 
    return props.map(node => { 
        // 打印当前节点 console.log(node); 
        if (node.children) { 
            return recursive(node.children); 
        } 
        return node; 
    }) 
}

寻找性能瓶颈

虽说程序员要对自己写的代码有自信,我自信这初始的技术实现是有问题的

初步实现了技术需求后,我想要测试一下自己写的代码性能怎么样?我们需要在火焰图中找到调用的方法。通过分析方法耗时,我们可以找到性能问题点

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

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

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

这样就可以清楚地看到哪些方法在拖慢程序的性能。这里可以看到总耗时在 67.15 ms

优化措施

分析

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

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

第一轮优化

  1. 将递归算法替换为 深度优先遍历。深度优先遍历通常在处理大量数据时具有更好的的性能表现
/** 具体的实现代码不便放在这里 */

// 深度优先遍历 伪代码
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 这两个属性)

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

这两步做完后,我们的总耗时下降到了 6.02 ms,但作为具有工匠精神的前端程序员,能不能对这段逻辑再进一步的优化?

第二轮细节优化

解决了主要问题点后,我们看下还有哪些小的耗时可以被优化。我们在放大火焰图,查看具体方法的耗时,发现第三方库 lodash.cloneDeep 深拷贝操作耗时有 0.38ms。如果是只调用一次,那肯定是没有什么问题,但是在我们这里的遍历中,会有很多次的调用,累计的耗时就会很长

使用JSON.parse(JSON.stringify())替换 lodash 的深拷贝操作。这种方法虽然可能在某些情况下存在局限性,但在大多数场景下性能更优

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

通过火焰图不仅可以查看到 lodash.cloneDeep 的耗时,还可以查看到其他方法的耗时,比如在本案例中,其实还优化 lodash.get 方法

// 根据业务特性,重写 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 浏览器的火焰图是一个非常实用的工具,可以帮助我们定位前端性能问题并进行优化。通过实际案例分析,我们可以发现优化方法的选择对性能提升具有重要影响。在进行前端性能优化时,不妨尝试参考火焰图来实施优化。