当前位置: 首页 > news >正文

前端性能优化实用方案(四):DOM批处理减少80%重排重绘

4 操作速度和渲染速度优化

前端性能优化不只是减少资源体积那么简单。当用户点击按钮、滚动页面时,如果响应慢半拍,再小的文件也救不了糟糕的体验。

这次我们聊聊如何让页面操作更流畅,让渲染更快速。

4.1 一次性操作大量DOM

DOM操作就像搬家,一件一件搬肯定比打包一起搬要累得多。浏览器每次重排(reflow)和重绘(repaint)都要消耗不少资源,所以批量处理是王道。

批量DOM操作

很多人写代码时习惯这样操作DOM:

// 这样写会让浏览器很累
function inefficientDOMUpdate(items) {const container = document.getElementById('list');items.forEach(item => {const div = document.createElement('div');div.textContent = item.name;container.appendChild(div); // 每次都要重新计算布局});
}// 改成这样,浏览器会轻松很多
function efficientDOMUpdate(items) {const fragment = document.createDocumentFragment();items.forEach(item => {const div = document.createElement('div');div.textContent = item.name;fragment.appendChild(div); // 先在内存里组装好});document.getElementById('list').appendChild(fragment); // 一口气插入
}

DocumentFragment就像一个临时容器,你可以在里面随意组装元素,最后一次性放到页面上。这样浏览器只需要重排一次,性能提升立竿见影。

虚拟滚动简化版

当数据量大到一定程度,比如几万条记录,就算批量操作也扛不住。这时候虚拟滚动就派上用场了。

class SimpleVirtualList {constructor(container, itemHeight = 50) {this.container = container;this.itemHeight = itemHeight;this.visibleCount = Math.ceil(container.clientHeight / itemHeight);this.startIndex = 0;container.addEventListener('scroll', this.handleScroll.bind(this));}render(items) {this.items = items;// 设置总高度,让滚动条显示正确this.container.style.height = items.length * this.itemHeight + 'px';this.updateVisibleItems();}handleScroll() {const newStartIndex = Math.floor(this.container.scrollTop / this.itemHeight);if (newStartIndex !== this.startIndex) {this.startIndex = newStartIndex;this.updateVisibleItems();}}updateVisibleItems() {const endIndex = Math.min(this.startIndex + this.visibleCount + 2, this.items.length);const visibleItems = this.items.slice(this.startIndex, endIndex);this.container.innerHTML = '';const fragment = document.createDocumentFragment();visibleItems.forEach((item, index) => {const div = document.createElement('div');div.style.position = 'absolute';div.style.top = (this.startIndex + index) * this.itemHeight + 'px';div.style.height = this.itemHeight + 'px';div.textContent = item.name;fragment.appendChild(div);});this.container.appendChild(fragment);}
}

虚拟滚动的核心思路很简单:只渲染用户能看到的部分,其他的用空白占位。用户滚动时动态更新可见区域的内容。

时间切片渲染

有时候数据不算特别多,但渲染逻辑比较复杂,一次性处理完会卡住界面。这时候可以用时间切片,把大任务拆成小块。

function timeSliceRender(items, renderFn, batchSize = 100) {let index = 0;function renderBatch() {const endIndex = Math.min(index + batchSize, items.length);for (let i = index; i < endIndex; i++) {renderFn(items[i], i);}index = endIndex;if (index < items.length) {// 让出控制权,让浏览器处理其他事情requestIdleCallback(renderBatch);}}renderBatch();
}// 使用起来很简单
const largeDataSet = Array.from({length: 10000}, (_, i) => ({id: i, name: `Item ${i}`}));
const container = document.getElementById('container');timeSliceRender(largeDataSet, (item) => {const div = document.createElement('div');div.textContent = item.name;container.appendChild(div);
}, 50);

requestIdleCallback会在浏览器空闲时执行回调,这样既能完成渲染任务,又不会阻塞用户交互。

4.2 避免复杂度很高的运算

复杂计算就像堵车,会让整个页面都卡住。用户点击按钮半天没反应,体验自然好不了。

循环优化

最常见的性能杀手就是嵌套循环。看看这个例子:

// 这种写法时间复杂度是O(n²),数据一多就完蛋
function inefficientLoop(data) {const results = [];for (let i = 0; i < data.length; i++) {for (let j = 0; j < data.length; j++) {if (data[i].category === data[j].category && i !== j) {results.push({item1: data[i], item2: data[j]});}}}return results;
}// 换个思路,先分组再处理,复杂度降到O(n)
function efficientLoop(data) {const categoryMap = new Map();const results = [];// 第一遍:按类别分组data.forEach(item => {if (!categoryMap.has(item.category)) {categoryMap.set(item.category, []);}categoryMap.get(item.category).push(item);});// 第二遍:在每个分组内部配对categoryMap.forEach(items => {for (let i = 0; i < items.length; i++) {for (let j = i + 1; j < items.length; j++) {results.push({item1: items[i], item2: items[j]});}}});return results;
}

同样的功能,优化后的版本在处理大数据时会快很多倍。关键是要选对数据结构,Map和Set在查找时比数组快得多。

计算缓存

有些计算结果可能会被重复使用,每次都重新算就太浪费了。

// 记忆化缓存,算过的结果直接返回
function memoize(fn) {const cache = new Map();return function(...args) {const key = JSON.stringify(args);if (cache.has(key)) {return cache.get(key);}const result = fn.apply(this, args);cache.set(key, result);return result;};
}// 假设这是个很耗时的计算
const expensiveCalculation = memoize((n) => {console.log('正在计算...', n);let result = 0;for (let i = 0; i < n * 1000000; i++) {result += Math.sqrt(i);}return result;
});// 第一次调用会计算,之后直接返回缓存
console.log(expensiveCalculation(100)); // 会看到"正在计算..."
console.log(expensiveCalculation(100)); // 直接返回,没有日志

缓存的威力在于,相同的输入永远返回相同的输出。这在处理复杂数学运算或数据转换时特别有用。

Web Worker异步计算

当计算真的很复杂,无法避免时,可以把它丢到Web Worker里去处理,这样主线程就不会被阻塞。

// worker.js - 在后台线程运行
self.onmessage = function(e) {const { data, operation } = e.data;let result;switch (operation) {case 'sum':result = data.reduce((acc, val) => acc + val, 0);break;case 'sort':result = data.sort((a, b) => a - b);break;case 'filter':result = data.filter(x => x % 2 === 0);break;default:result = data;}self.postMessage(result);
};
// 主线程的Worker封装
class WorkerHelper {constructor(workerScript) {this.worker = new Worker(workerScript);}calculate(data, operation) {return new Promise((resolve, reject) => {this.worker.onmessage = (e) => resolve(e.data);this.worker.onerror = (e) => reject(e);this.worker.postMessage({ data, operation });});}terminate() {this.worker.terminate();}
}// 用起来就像普通的异步函数
const workerHelper = new WorkerHelper('./workers/calculator.js');
const largeArray = Array.from({length: 1000000}, (_, i) => i);workerHelper.calculate(largeArray, 'sum').then(result => console.log('计算完成:', result)).catch(error => console.error('出错了:', error));

Web Worker的好处是真正的并行计算,主线程该干嘛干嘛,计算完了再通知结果。

防抖和节流优化

用户操作往往很频繁,比如输入搜索关键词、滚动页面。如果每次操作都触发复杂逻辑,性能肯定扛不住。

// 防抖:等用户停止操作后再执行
function debounce(func, wait) {let timeout;return function executedFunction(...args) {const later = () => {clearTimeout(timeout);func(...args);};clearTimeout(timeout);timeout = setTimeout(later, wait);};
}// 节流:限制执行频率
function throttle(func, limit) {let inThrottle;return function(...args) {if (!inThrottle) {func.apply(this, args);inThrottle = true;setTimeout(() => inThrottle = false, limit);}};
}// 搜索建议:用户停止输入300ms后再搜索
const expensiveSearch = debounce((query) => {console.log('开始搜索:', query);// 这里放复杂的搜索逻辑
}, 300);// 滚动监听:最多100ms执行一次
const handleScroll = throttle(() => {console.log('当前滚动位置:', window.scrollY);// 这里放滚动处理逻辑
}, 100);document.getElementById('search').addEventListener('input', (e) => {expensiveSearch(e.target.value);
});window.addEventListener('scroll', handleScroll);

防抖适合搜索、表单验证这种"等用户操作完再处理"的场景。节流适合滚动、拖拽这种"控制执行频率"的场景。

算法复杂度优化

有时候换个算法思路,性能能提升几个数量级。

// 找重复项的低效方法:O(n²)
function findDuplicatesInefficient(arr) {const duplicates = [];for (let i = 0; i < arr.length; i++) {for (let j = i + 1; j < arr.length; j++) {if (arr[i] === arr[j] && !duplicates.includes(arr[i])) {duplicates.push(arr[i]);}}}return duplicates;
}// 高效方法:O(n)
function findDuplicatesEfficient(arr) {const seen = new Set();const duplicates = new Set();for (const item of arr) {if (seen.has(item)) {duplicates.add(item);} else {seen.add(item);}}return Array.from(duplicates);
}// 性能差距有多大?测试一下就知道
const testArray = Array.from({length: 10000}, () => Math.floor(Math.random() * 5000));console.time('低效算法');
findDuplicatesInefficient(testArray);
console.timeEnd('低效算法');console.time('高效算法');
findDuplicatesEfficient(testArray);
console.timeEnd('高效算法');

数据量小的时候可能看不出差别,但当数组有几万个元素时,差距就很明显了。

4.3 性能监控

光优化还不够,还要能监控到性能问题。

// 简单的性能监控工具
class PerformanceMonitor {static measureFunction(fn, name) {return function(...args) {const start = performance.now();const result = fn.apply(this, args);const end = performance.now();console.log(`${name} 耗时: ${(end - start).toFixed(2)}ms`);return result;};}static measureAsync(fn, name) {return async function(...args) {const start = performance.now();const result = await fn.apply(this, args);const end = performance.now();console.log(`${name} 耗时: ${(end - start).toFixed(2)}ms`);return result;};}
}// 包装一下就能看到执行时间
const optimizedFunction = PerformanceMonitor.measureFunction(efficientLoop, '优化后的循环'
);optimizedFunction(testData);

这样就能直观地看到哪些函数比较慢,需要进一步优化。

小结

操作和渲染速度优化说到底就是几个原则:

减少不必要的工作 - 批量DOM操作、虚拟滚动、时间切片,能少做就少做。

选对算法和数据结构 - Map比数组查找快,O(n)比O(n²)快,这些基础要扎实。

合理使用缓存 - 算过的结果就别重复算了,内存换时间很划算。

异步处理重任务 - Web Worker、防抖节流,别让复杂计算阻塞用户操作。

持续监控性能 - 没有监控就没有优化,要知道瓶颈在哪里。

性能优化是个持续的过程,不是一次性的工作。关键是要根据实际情况选择合适的策略,过度优化有时候比不优化还糟糕。

http://www.dtcms.com/a/391925.html

相关文章:

  • 速通ACM省铜第九天 赋源码(Divine Tree)
  • win10程序(七)暴力xls转xlsx程序
  • PINN物理信息神经网络驱动的Burgers偏微分方程求解MATLAB代码
  • Linux系统多线程的同步问题
  • Anaconda下载及使用详细教程
  • 第二部分:VTK核心类详解(第43章 vtkCharArray字符数组类)
  • 2025年9月19日NSSCTF之[陇剑杯 2021]日志分析(问1)
  • TDesign学习:(五)设置三级菜单的坑
  • 两步构建企业级AI知识库:技术实战与权限管理指南
  • 乐华显示工业一体机 10 大维护要点
  • 【MySQL ✨】MySQL 入门之旅 · 第七篇:MySQL 更新与删除数据(UPDATE / DELETE 语句)
  • 关于C++的入门基础
  • TDengine 标准差 STDDEV 函数使用场景及意义
  • color-printf一个轻量级、高兼容的终端彩色打印工具
  • Python实现等离子体反应优化 (Plasma Generation Optimization, PGO)(附完整代码)
  • 【C++】vector
  • LeeCode120. 三角形最小路径和
  • 元启发式算法分类
  • Ansible-file模块
  • Vue项目使用Coze的聊天窗(一)
  • 关于将tomcat、nginx 实现 注册window服务并实现自启动
  • 【精品资料鉴赏】358页 数字政府大数据中心资源平台治理建设方案
  • 关于Spring Bean之间的循环依赖
  • pake将前端web项目打包成windows可安装文件
  • 低轨卫星应用:MCU、CANFD与DCDC芯片的集成解决方案
  • AI 编程Claude Code使用详细教程
  • vue3 下载文件方式(包括通过url下载文件并修改文件名称,和文件流下载方式)
  • 如何高效筛选海量文献,避免浪费时间?
  • heyday
  • Go语言结构体初始化全面指南与最佳实践