Menu菜单组件设计与前端交互优化实战
项目初期的技术选型
上个月接了个后台管理系统的活,UI 框架用的是 Ant Design,菜单这块自然就用了 Menu 组件。说实话,一开始觉得这不就是个现成的组件嘛,配置几个路由、加点图标,搞定收工。结果越做越发现不对劲——需求比想象中复杂多了。
客户要求支持三级菜单,而且有些菜单项要动态加载(比如权限不同看到的不一样),还要支持收缩展开、记住上次打开的状态。最头疼的是,菜单数据是从后端接口拿的,不是写死的静态配置。我一开始图省事,直接在 useEffect 里 fetch 数据,然后塞进 Menu 的 items 属性,结果页面一加载就卡得不行,尤其是菜单项多的时候,浏览器直接卡几秒。
最大的坑:性能问题
折腾了半天才发现,问题出在每次状态更新都会触发整个 Menu 重新渲染。因为我是把菜单数据存在 React 的 state 里,一旦有子菜单展开/收起,或者用户切换了主题色(影响菜单样式),整个菜单树都会被重建。Ant Design 的 Menu 虽然功能全,但内部实现对大型菜单确实不太友好。
开始没想到这么严重,直到 QA 同学提了个 bug:「菜单超过 50 项时,点击展开有明显延迟」。我本地测了下,100 项菜单,展开一个子菜单居然要 300ms+,这肯定不行。
我试过几种方案:
- 用
React.memo包裹菜单项 —— 没用,因为Menu是整体组件,内部已经做了优化,外部 memo 无效 - 把菜单数据缓存到 Redux —— 有点用,但首次加载还是慢
- 改成虚拟滚动?—— 查了文档,Ant Design 的 Menu 不支持虚拟滚动,得自己造轮子,成本太高
最后灵机一动:既然问题出在「每次更新都重建整个菜单树」,那能不能只让变化的部分重新渲染?于是我把菜单拆成了两层:顶层用静态配置(比如首页、设置这些固定入口),动态部分单独用一个子组件,只在需要时才加载。
核心代码就这几行
关键在于「懒加载」和「局部状态隔离」。我把动态菜单封装成一个独立的组件 DynamicMenu,它只负责渲染从 API 拿到的那部分菜单,并且用 useMemo 缓存转换后的 AntD 格式数据。这样即使父组件 re-render,只要菜单数据没变,DynamicMenu 就不会重新计算。
// DynamicMenu.jsx
import { useState, useEffect, useMemo } from 'react';
import { Menu } from 'antd';
const transformToMenuItems = (data) => {
return data.map(item => ({
key: item.id,
label: item.name,
children: item.children ? transformToMenuItems(item.children) : undefined,
}));
};
const DynamicMenu = ({ menuData }) => {
const memoizedItems = useMemo(() => {
return transformToMenuItems(menuData);
}, [menuData]);
return <Menu items={memoizedItems} mode="inline" />;
};
export default DynamicMenu;
然后在主布局里,只在需要时才渲染这个组件:
// Layout.jsx
import { useState, useEffect } from 'react';
import { Menu } from 'antd';
import DynamicMenu from './DynamicMenu';
const Layout = () => {
const [dynamicMenuData, setDynamicMenuData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 只在首次加载时请求
fetch('https://jztheme.com/api/menu')
.then(res => res.json())
.then(data => {
setDynamicMenuData(data);
setLoading(false);
});
}, []);
return (
<div className="layout">
<Menu mode="inline" selectable={false}>
<Menu.Item key="home">首页</Menu.Item>
<Menu.Item key="profile">个人中心</Menu.Item>
{/* 静态菜单结束 */}
{!loading && dynamicMenuData && <DynamicMenu menuData={dynamicMenuData} />}
</Menu>
</div>
);
};
这样改完后,菜单展开速度从 300ms+ 降到了 30ms 以内,基本感觉不到卡顿了。亲测有效。
踩坑提醒:这三点一定注意
除了性能,还有几个小坑差点让我翻车:
- key 值必须稳定:一开始我用数组索引当 key,结果动态增删菜单时 UI 错乱。后来改成用后端返回的唯一 ID,问题解决。
- openKeys 和 selectedKeys 要分开管理:AntD 的 Menu 里,
openKeys控制哪些子菜单展开,selectedKeys控制哪个菜单项高亮。我一开始混在一起处理,导致点击子菜单时父级自动收起了。后来拆成两个 state,分别监听onOpenChange和onSelect,逻辑清晰多了。 - 收缩模式下的图标处理:菜单收缩时,AntD 默认只显示图标。但我们的设计要求收缩时显示文字缩写(比如“系统设置”变成“设”)。这得自己 override 样式 + 自定义 render,不能直接用默认行为。
另外,菜单权限控制也费了点功夫。我们是在前端根据角色过滤菜单项,而不是后端返回。好处是灵活,坏处是首次加载还是会拉全量数据(虽然不渲染)。如果数据量特别大,建议还是后端做过滤。
回顾与反思
现在回头看,这个方案虽然解决了性能问题,但也不是完美的。比如动态菜单的加载状态处理得有点糙,目前是整个区域 loading,其实可以按需加载子菜单(点击父级再请求子级)。不过考虑到项目时间紧,而且菜单结构相对固定,就没继续优化。
另外,菜单的「记住上次展开状态」功能也没完全做好。现在是用 localStorage 存 openKeys,但用户切换账号后没清理,偶尔会串数据。这个问题不大,但确实是个隐患,后续版本得补上。
总的来说,这次踩坑让我意识到:看似简单的组件,一旦数据量上去或交互复杂,就得深入看源码、做性能分析。不能光依赖 UI 库的默认行为。AntD 的 Menu 功能强大,但用在大型项目里,一定要做二次封装和优化。
以上是我个人对这个菜单组件的完整折腾过程,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如结合路由守卫做权限校验),后续会继续分享这类博客。希望对你有帮助!

暂无评论