优化HTMLLIElement数组CSS类更新操作的执行性能
大列表搜索过滤卡顿优化
问题描述
我有一个存储HTMLLIElement的数组,需要根据搜索输入框的取值给对应元素设置CSS类。当数组元素量较大时,页面会出现卡顿、响应迟缓的问题,经定位性能瓶颈来自每个li元素的文本比对逻辑,初始实现代码如下:
// 获取搜索输入值转小写,拿到选择列表下所有li转为数组 const searchValue = myHappySeachInput.toLowerCase(); const selectListItems = myHappySelectList.querySelectorAll("li"); const myHappyArray = Array.from(selectListItems); // 遍历每个li,比对搜索值和元素内文本,按需增删CSS类 myHappyArray.forEach((li: HTMLLIElement) => { const label = li.querySelector("label"); const text = label ? label.innerText.toLowerCase() : ""; if (text.indexOf(searchValue) < 0) { li.classList.add("display-none"); } else { li.classList.remove("display-none"); } });
后续我参考建议拆分了查询匹配和类名赋值逻辑,做了第一版优化:
// 先给所有li加上隐藏类 myHappyArray.forEach((li) => li.classList.add("display-none")); // 过滤出所有匹配搜索值的li const matchingLiElements = myHappyArray.filter((li) => li.querySelector("label")?.innerText.toLowerCase().includes(searchValue)); // 仅给匹配的元素移除隐藏类 matchingLiElements.forEach((li) => li.classList.remove("display-none"));
这版优化已经将执行耗时降低了80%-90%,但仍有缺陷:每次触发搜索时还是需要更新全量数组元素的CSS类,希望能找到更优的实现方案。
进一步优化方案
核心优化思路是砍掉重复计算、缩小DOM操作范围,可以从这几个点落地:
- 缓存静态数据:列表首次渲染完成后只遍历一次所有li,提前把每个li对应的label引用、label文本转小写后的结果缓存好(可以挂在元素自定义属性上,也可以用
WeakMap存储,避免内存泄漏),后续搜索时直接读缓存,完全省掉每次搜索触发的querySelectorDOM查询、innerText取值、小写转换这类重复开销 - 增量更新DOM:记录上一次搜索的匹配元素集合,本次搜索完成后,仅对两类元素做类名修改:①上次匹配、本次不匹配的元素,加隐藏类;②本次匹配、上次未匹配的元素,移除隐藏类。状态没有变化的元素完全不需要操作,彻底避免全量修改类名的开销
- 加搜索防抖:给输入框的搜索回调加100-200ms的防抖,用户连续快速输入时不执行过滤逻辑,等输入停顿后再触发计算,减少无意义的执行次数
- 超大量列表优化重排:如果列表元素量级达到万级,可以在批量修改类名前先把列表父容器设为
display: none,所有类名修改完成后再恢复显示,把多次重排合并为一次
参考实现代码:
// 初始化阶段仅执行一次:缓存搜索用的文本 const selectListItems = myHappySelectList.querySelectorAll("li"); const myHappyArray = Array.from(selectListItems) as HTMLLIElement[]; const textCache = new WeakMap<HTMLLIElement, string>(); myHappyArray.forEach(li => { const label = li.querySelector("label"); textCache.set(li, label ? label.innerText.toLowerCase() : ""); }); // 记录上一次的匹配集合 let lastMatchedSet = new Set<HTMLLIElement>(); // 搜索逻辑,绑定到输入框防抖后的回调上 function doSearch(inputValue: string) { const searchValue = inputValue.toLowerCase(); // 匹配过程无任何DOM操作,直接读缓存 const currentMatchedSet = new Set( myHappyArray.filter(li => textCache.get(li)!.includes(searchValue)) ); // 仅操作状态变化的元素 lastMatchedSet.forEach(li => { if (!currentMatchedSet.has(li)) { li.classList.add("display-none"); } }); currentMatchedSet.forEach(li => { if (!lastMatchedSet.has(li)) { li.classList.remove("display-none"); } }); lastMatchedSet = currentMatchedSet; }
这套实现下,搜索时的DOM操作量会随着搜索结果的变化量缩小,绝大多数场景下只需要操作极少量元素,哪怕是上万条的列表也不会出现卡顿。
内容的提问来源于stack exchange,提问作者Veckodag
相关产品推荐
相关产品推荐

