一次紧急的现场性能问题排查

问题由来

突然接到现场反馈,软件的模型树操作非常卡顿,客户要求立即排查解决问题。与客户沟通发现此问题存在于协同项目(另一种是本地建模的项目),且在项目包非常多的时候才会出现,并非所有项目都是如此卡顿。

尝试复现客户场景,建了几千个包。然后打开对应的协同项目,发现当滚动模型树,或者展开模型节点的时候非常卡,完全没法操作的。

开始排查

还好之前做过性能优化相关的,打开开发者工具:

  1. 切换性能面板,点击录制。
  2. 查看火焰图,发现一直有长任务执行。浏览器中长任务持续占用 JS 线程,因 JS 与渲染线程互斥,渲染线程无法工作,且阻塞事件循环,致使页面无法渲染、用户交互无响应。
  3. 选中对应执行任务( freshPackageStatus),查看摘要,点击来源中的文件地址,查看长任务的执行的地方。

经过排查发现这里是发起了一个 http 请求,接口返回了当前全量的模型树节点,然后做了一个逻辑处理,并会触发视图更新。最要命的是,这个请求还是前端轮询请求的。轮询的时间是 10s,但是从火焰图中可以看到,这里的长任务耗时是 16s 。所以整个页面处于完全卡死的状态。

解决办法

socket 消息通知

将轮询改为 socket 通知,我们项目本身就接入了 socket ,将轮询接口改为,当我收到指定的消息时候才去执行 http 请求。

这一步处理起来并不难,也确实解决了用户卡死的问题,但还有问题存在。当协同端触发消息通知的时候,前端还是会卡死一段时间,这个前端 16s 的逻辑执行时间并不会变短,只是减少了它触发的频率。

拆分长任务

当前采用的是分块处理和异步更新的方式,其中核心代码如下

ini 复制代码
  async processItemsInBatches(items, project:Project, batchSize = 200) {
    for (let i = 0; i < items.length; i += batchSize) {
      const batch = items.slice(i, i + batchSize);
      const packageStatus = {} as Record<string, CoworkPackageStatus>;

      for (let item of batch) {
        packageStatus[item.PACKAGE_ID] = { ...item };
      }

      project.setPackageStatus(packageStatus);
      await new Promise(resolve => setTimeout(resolve, 0)); // 延迟一小段时间
    }
  }

分块处理的本质确实是利用事件循环机制来避免长时间阻塞主线程。JavaScript是单线程的,其执行模型基于事件循环:

主线程任务 → 微任务 → 渲染 → 宏任务 → 主线程任务...

当有大量同步代码执行时(比如forEach循环),会形成"长任务",分块处理通过setTimeoutrequestIdleCallback将任务拆分为多个宏任务,让事件循环有机会在任务之间处理其他事件和渲染。

batchSize 设置的合理性

可以根据不同的设备计算出合适的 batchSize ,我这边就图方便设置了一个固定的大小。

经过优化后可以看到每个任务都被拆分

现在的执行流程是

  • processItemsInBatches 分块处理任务
    • setPackageStatus 更新视图状态
    • 触发视图更新

再近一步优化视图更新

发现有很多重复的计算,并且组件渲染时间比较长。

主要问题如下:

  1. 每次遍历都去对比 newStatus 和 packageStatus 造成不必要的计算。
  2. status 本身是一个很大的对象,每次对比都是一个比较耗时的计算,叠加第一条一起就造成了大量的重复的 js计算。 相关逻辑代码如下
ts 复制代码
   const newStatus = {}
   result.data?.forEach(item => {
    newStatus[item.PACKAGE_ID] = {
      ...item,
    };
    project.setPackageStatus(newStatus);
  });


  setPackageStatus(status: { [p: string]: CoworkPackageStatus }) {
      if (!this.isPackageStatusSame(this.config.onlineConfig.packageStatus, status)) {
        this.config.onlineConfig.packageStatus = status;
        // ...
      }
  }


  isPackageStatusSame(status1:{ [p: string]: CoworkPackageStatus }, status2:{ [p: string]: CoworkPackageStatus }) {
    const keys = Object.keys(status1);
    if (keys.length !== Object.keys(status2).length) {
      return false;
    }
    for (const key of keys) {
      if (!status2[key]) return false;
      const old = status1[key];
      const newItem = status2[key];
      if(条件判断) {
        return false;
      }

    }

    return true;
  }
  1. 树节点组件依赖 packageStatus 状态, 一棵树可能几千上万个树节点,上面的 setPackageStatus 是重新修改了 packageStatus 的引用,导致每个节点组件的 computed 函数都要重新计算一遍。如果状态发生变更的话需要重新更新节点视图。
kotlin 复制代码
 computed: {
    packageLockedIcon() {
      const { id } = this.graphNode.data;
      const packageStatus = this.graphNode.project?.config.onlineConfig?.packageStatus;
      if (packageStatus && packageStatus[id] && packageStatus[id].LOCKED) {
        return true;
      }
      return false;
    }
 }

解决思路

  1. 构建完成 newStatus 再去做更新
ini 复制代码
   const newStatus = {}
   // 构建 newStatus
   result.data?.forEach(item => {
      newStatus[item.PACKAGE_ID] = {
        ...item,
      };
    });
   // 完成后再去做更新
   project.setPackageStatus(newStatus);
  1. 优化对比算法,来比较对应模型相关的数据是否一致 3. 避免直接修改 packageStatus 对象引用,可以增量的变更/删除对象相关的属性

经过上面的优化后大大减少 js 计算的时间,以及渲染相关的时间。效果如下图:

总结

排查问题思路

  1. 通过performance定位主要性能卡点,主要为10s一次的定时器请求及回调函数中有长任务
  2. 通过socket解决定时器轮训状态问题
  3. 定位长任务中原因为频繁触发大对象的比对。
  4. 合并比对次数,以及优化比对算法。
  5. 最终减少了大多数的js计算的时间,并且减少了视图渲染次数。
相关推荐
四月_h4 分钟前
vue2动态实现多Y轴echarts图表,及节点点击事件
前端·javascript·vue.js·echarts
文心快码BaiduComate27 分钟前
用Zulu轻松搭建国庆旅行4行诗网站
前端·javascript·后端
行者..................2 小时前
手动编译 OpenCV 4.1.0 源码,生成 ARM64 动态库 (.so),然后在 Petalinux 中打包使用。
前端·webpack·node.js
小爱同学_2 小时前
一次面试让我重新认识了 Cursor
前端·面试·程序员
golang学习记2 小时前
AI 乱写代码?不是模型不行,而是你的 VS Code 缺了 Context!MCP 才是破局关键
前端
星光不问赶路人3 小时前
Vite 中的 import.meta.glob vs 动态导入:该用哪个?
前端·vite
z_y_j2299704383 小时前
服务器中使用Docker部署前端项目
服务器·前端·docker·容器
迪丽热爱3 小时前
解决【npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。】问题
前端·npm·node.js
数字冰雹3 小时前
图观 流渲染场景服务器
服务器·前端·数据库·数据可视化
李明卫杭州4 小时前
详细讲解js中的ResizeObserver
前端·javascript