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

举报
Yeats_Liao 发表于 2025/11/16 19:16:32 2025/11/16
【摘要】 4 操作速度和渲染速度优化前端性能优化不只是减少资源体积那么简单。当用户点击按钮、滚动页面时,如果响应慢半拍,再小的文件也救不了糟糕的体验。这次我们聊聊如何让页面操作更流畅,让渲染更快速。 4.1 一次性操作大量DOMDOM操作就像搬家,一件一件搬肯定比打包一起搬要累得多。浏览器每次重排(reflow)和重绘(repaint)都要消耗不少资源,所以批量处理是王道。 批量DOM操作很多人写代...

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、防抖节流,别让复杂计算阻塞用户操作。

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

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

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。