从零搭建到优化实战 一份详尽的Menu菜单组件开发指南
优化前:卡得不行
大家好,今天来聊聊我最近在项目里优化的一个菜单组件。这个菜单组件本来用起来挺方便的,但是一到数据量大了就卡得不行。每次展开和收起菜单的时候,页面都像是在放慢动作电影,加载时间经常超过5秒,用户体验简直差到爆。
找到瞄颈了!
一开始我以为是网络请求的问题,后来发现响应时间其实还挺快的,问题出在前端渲染上。我用了Chrome DevTools的Performance面板,定位到渲染瓶颈。结果发现,每次展开和收起菜单的时候,大量的DOM操作导致了性能瓶颈。
具体来说,菜单组件里面有大量的子菜单项,每次展开和收起都会重新渲染整个菜单树,这显然是个很大的性能开销。
优化后:流畅多了
试了几种方案,最后这个效果最好。核心优化方法主要是减少不必要的DOM操作,以及使用虚拟列表来优化长列表的渲染。
1. 减少不必要的DOM操作
优化前,每次展开和收起菜单时,都会重新渲染整个菜单树。这样不仅浪费性能,而且用户体验也很糟糕。我决定采用局部更新的方式,只更新发生变化的部分。
// 优化前的代码
function renderMenu(menuData) {
const menuContainer = document.getElementById('menu-container');
menuContainer.innerHTML = ''; // 清空容器
menuData.forEach(item => {
const menuItem = createMenuItem(item);
menuContainer.appendChild(menuItem);
});
}
// 优化后的代码
function updateMenu(menuData, currentMenu) {
const menuContainer = document.getElementById('menu-container');
const newMenu = new Map();
menuData.forEach(item => {
const key = item.id;
newMenu.set(key, item);
});
for (const [key, newItem] of newMenu) {
if (currentMenu.has(key)) {
const existingItem = currentMenu.get(key);
if (existingItem !== newItem) {
const menuItem = createMenuItem(newItem);
menuContainer.replaceChild(menuItem, existingItem.element);
}
} else {
const menuItem = createMenuItem(newItem);
menuContainer.appendChild(menuItem);
}
}
for (const [key, oldItem] of currentMenu) {
if (!newMenu.has(key)) {
menuContainer.removeChild(oldItem.element);
}
}
return newMenu;
}
这里注意,我踩过好几次坑,主要是因为DOM操作的顺序和条件判断。折腾了半天发现,使用Map来存储当前菜单项的状态是个不错的方法,可以快速查找和更新。
2. 使用虚拟列表
对于长列表的渲染,我引入了虚拟列表的概念。虚拟列表只渲染可视区域内的元素,而不是一次性渲染所有元素,这样可以大大减少DOM节点的数量,提升性能。
// 虚拟列表的基本实现
class VirtualList {
constructor(element, items, itemHeight) {
this.element = element;
this.items = items;
this.itemHeight = itemHeight;
this.renderedItems = [];
this.scrollTo(0);
}
scrollTo(scrollTop) {
const containerHeight = this.element.clientHeight;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.ceil((scrollTop + containerHeight) / this.itemHeight);
for (let i = 0; i < this.renderedItems.length; i++) {
const renderedItem = this.renderedItems[i];
if (renderedItem.index = endIndex) {
this.element.removeChild(renderedItem.element);
this.renderedItems.splice(i, 1);
i--;
}
}
for (let i = startIndex; i ri.index === i);
if (renderedItemIndex === -1) {
const element = this.createItemElement(item, i);
this.element.appendChild(element);
this.renderedItems.push({ index: i, element });
} else {
this.renderedItems[renderedItemIndex].element.style.transform = `translateY(${i * this.itemHeight}px)`;
}
}
}
createItemElement(item, index) {
const element = document.createElement('div');
element.style.height = `${this.itemHeight}px`;
element.style.transform = `translateY(${index * this.itemHeight}px)`;
element.textContent = item.text;
return element;
}
}
// 使用虚拟列表
const menuData = fetch('https://jztheme.com/api/menu-data').then(response => response.json()).then(data => {
const menuContainer = document.getElementById('menu-container');
const virtualList = new VirtualList(menuContainer, data, 50);
menuContainer.addEventListener('scroll', () => {
virtualList.scrollTo(menuContainer.scrollTop);
});
});
虚拟列表的实现确实需要一些额外的工作,但是效果非常显著。加载时间从5秒降到800ms,用户体验得到了极大的提升。
性能数据对比
为了验证优化效果,我做了一些性能测试。以下是优化前后的性能数据对比:
- 优化前: 页面加载时间5秒,CPU占用率较高,滚动时有明显的卡顿。
- 优化后: 页面加载时间800ms,CPU占用率明显降低,滚动流畅无卡顿。
总结
以上是我的优化经验,希望能对你有所帮助。优化过程中确实遇到了不少坑,但最终还是找到了合适的解决方案。如果你有更好的方案或建议,欢迎在评论区交流讨论。
这个技巧的拓展用法还有很多,后续我会继续分享这类博客。希望我的踩坑经历能让你少走弯路。

暂无评论