前端登录菜单加载性能优化总结

前端登录菜单加载性能优化总结

问题现象

登录后页面出现明显卡顿,接口返回约 900 条菜单权限数据,数据量不大但前端处理耗时明显,主线程被阻塞。

瓶颈分析

整个调用链路:获取菜单接口 → 扁平数组转树形结构 → 存储菜单状态 → 解析路由和权限

排查后发现有两处性能问题:

瓶颈一:扁平数组转树形结构 --- O(N²) 复杂度

问题 :内层递归函数对每个节点都遍历整个剩余数组查找子节点,加上 Array.splice() 从数组中间删除元素,整体时间复杂度为 O(N²)。

js 复制代码
// 原实现(简化)
function arrayToTree(parent, level) {
  var k = list.length - 1;
  while (k >= 0) {
    if (parent.id === list[k].pid) {
      children.push(list[k]);
      list.splice(k, 1);        // O(N) 删除
      arrayToTree(list[k], level + 1); // 递归后又从头遍历
    }
    k--;
  }
}

优化 :一次遍历建立 pid → children[] 的 Map 索引,递归时通过 Map 直接查找子节点,时间复杂度降为 O(N)。

js 复制代码
// 优化后
var childrenMap = {};
items.forEach(function (item) {
  var pid = item[option.pid].toString();
  if (!childrenMap[pid]) childrenMap[pid] = [];
  childrenMap[pid].push(item);
});

function buildTree(parent, level) {
  var children = childrenMap[parent[option.id].toString()];
  if (!children) return;
  // 直接通过 Map 拿到子节点,无需遍历整个数组
}

瓶颈二:循环内反复触发框架级 API(动态路由注册 + 状态更新)

问题:这是最主要的瓶颈。递归遍历 900 个节点时,每遇到符合条件的节点就同步调用:

  1. router.addRoute() --- 每次调用都会触发路由匹配器重新编译,N 次调用相当于 O(N²) 的编译开销
  2. store.commit() --- 每次提交都用扩展运算符 [...oldArr, ...newArr] 创建新数组,随着累积数组越来越大,开销递增
js 复制代码
// 原实现(简化)
data.reduce((pre, cur) => {
  // 每个节点都触发一次框架 API
  router.addRoute('layout', { ... });     // 重建路由匹配表
  store.commit('UPDATE_MENU', { ... });   // 展开合并数组
  if (cur.children.length) traverse(cur.children, ...);
}, []);

优化:先收集,后批量操作。遍历过程中只往普通数组/对象中 push 数据,遍历完成后统一执行副作用。

js 复制代码
// 优化后
var pendingRoutes = [];
var pendingMenus = {};

function collect(data, ...) {
  // 遍历中只收集数据,不调用框架 API
  pendingRoutes.push({ ... });
  pendingMenus[key] = (pendingMenus[key] || []).concat(items);
}

collect(data, ...);

// 遍历完成后批量添加路由
pendingRoutes.forEach(route => router.addRoute('layout', route));

// 一次性合并菜单数据到状态树
Object.keys(pendingMenus).forEach(key => { ... });

优化效果

环节 优化前 优化后
数组转树 O(N²),含 splice 删除 O(N),Map 索引查找
动态路由注册 每个节点调用一次,反复重建匹配表 收集后批量添加
状态更新 每个节点 commit 一次,反复展开数组 收集后一次性合并

经验教训

  1. 数据量不大 ≠ 不会慢:几百条数据看似不多,但 O(N²) 算法 + 循环内触发框架重计算,开销会被成倍放大
  2. 注意框架 API 的隐性成本router.addRoute 不是简单的数组 push,每次调用都有路由匹配器重编译的开销;store.commit 如果涉及数组展开合并,高频调用同样代价不小。应避免在循环中高频调用这类 API
  3. 先收集后批量是处理这类问题的通用模式:把"遍历"和"副作用"分离,遍历阶段只做纯数据收集,副作用(路由注册、状态提交、DOM 操作等)统一在最后批量执行
相关推荐
Revolution6120 分钟前
React 组件重新渲染时,到底重新执行了什么
前端·react.js·面试
用户2181697049302 小时前
Flutter(四)Dart语法 空安全 运算符 流程控制
前端
许彰午2 小时前
政务督办的分合模式:主办协办的并发审批
前端·javascript·政务
用户69371750013842 小时前
AI时代,程序员该往哪走?
前端·后端
ttwuai2 小时前
GoFrame 后台日志清空失败:无 WHERE 删除为什么被拦住
前端·golang
易筋紫容2 小时前
创建型模式:对象的诞生艺术
开发语言·前端·javascript
半句唐诗3 小时前
我是如何通过 Access Token 成功发布第一个 npm 包的
前端·npm·node.js
程序员黑豆3 小时前
鸿蒙应用开发:@Provider 与 @Consumer 跨组件双向同步详解
前端·harmonyos
paopaokaka_luck3 小时前
基于springboot3+vue3的智能文库平台(AI智能搜索、AI智能汇总、实时在线状态展示、多格式文档预览与富文本编辑、Echarts图形化分析)
前端·网络·spring boot·网络协议·echarts
牧艺3 小时前
cos-design PhotoAlbum:用 CSS 3D 做一个「能翻页」的实体相册
前端·css·交互设计