前端性能优化-第三篇(JavaScript执行优化)

摘要: JavaScript是单线程的,低效的JavaScript代码会阻塞主线程,导致页面卡顿和无响应。本篇聚焦于编写高效的JavaScript,让主线程快速"空闲"出来,确保交互的流畅性。

核心内容

1. 避免长任务(Long Tasks)与任务分解

任何在主线程上连续执行超过 50毫秒 的任务都被视为"长任务",它会阻塞用户交互(如点击、滚动)的响应。

优化策略:任务分解 。将一个大任务拆分成多个小任务,通过 setTimeoutsetIntervalrequestIdleCallback 分片执行。

示例:处理一个大型数组

javascript 复制代码
// 优化前:一个长任务
function processAllItems(items) {
  for (let i = 0; i < items.length; i++) {
    // 假设这是一个很耗时的处理
    heavyProcessing(items[i]);
  }
}

// 优化后:分解任务,避免阻塞主线程
function processItemsAsync(items) {
  let index = 0;
  
  function processNextChunk() {
    const startTime = performance.now();
    // 每次处理一小批,例如50个
    while (index < items.length && performance.now() - startTime < 50) {
      heavyProcessing(items[index]);
      index++;
    }
    
    if (index < items.length) {
      // 如果还有剩余项,将下一个分片放到任务队列末尾
      setTimeout(processNextChunk, 0);
    }
  }
  
  processNextChunk();
}

2. 防抖(Debounce)与节流(Throttle)

这两个技术是控制函数执行频率的利器。

防抖 (Debounce) :事件停止触发后,延迟一段时间再执行。最后一次操作才真正执行

  • 适用场景 :搜索框输入联想、窗口 resize 结束后的布局调整。

节流 (Throttle) :在一定时间间隔内,只执行一次。按固定频率执行

  • 适用场景 :页面滚动 scroll、鼠标移动 mousemove、频繁点击按钮。

示例实现:

javascript 复制代码
// 防抖实现
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);
    }
  };
}

// 使用示例
const searchInput = document.getElementById('search');
// 用户停止输入300ms后才执行搜索
searchInput.addEventListener('input', debounce(function(e) {
  console.log('发起搜索请求:', e.target.value);
}, 300));

// 滚动时,每100ms最多执行一次
window.addEventListener('scroll', throttle(function() {
  console.log('计算位置,懒加载图片');
}, 100));

3. 使用 requestAnimationFrame 进行动画

requestAnimationFrame (rAF) 是浏览器为动画专门提供的API,它会在下一次浏览器重绘之前执行回调函数,保证动画与浏览器的刷新率同步(通常是60fps)。

示例:一个平滑的动画

javascript 复制代码
// 使用 rAF 的流畅动画
function animateWithRAF(element, duration) {
  const start = performance.now();
  const from = 0;
  const to = 500;
  
  function step(timestamp) {
    const progress = Math.min((timestamp - start) / duration, 1);
    element.style.transform = `translateX(${from + (to - from) * progress}px)`;
    
    if (progress < 1) {
      requestAnimationFrame(step);
    }
  }
  
  requestAnimationFrame(step);
}

// 对比:不要这样写(使用setInterval的卡顿动画)
function animateWithInterval(element, duration) {
  const start = Date.now();
  const from = 0;
  const to = 500;
  
  const timer = setInterval(() => {
    const progress = Math.min((Date.now() - start) / duration, 1);
    element.style.left = `${from + (to - from) * progress}px`;
    
    if (progress >= 1) {
      clearInterval(timer);
    }
  }, 1000 / 60); // 试图模拟60fps
}

关键区别rAF 在浏览器渲染前执行,而 setInterval 的时间不精确,可能在帧中间执行,导致丢帧或过度渲染。

4. Web Workers 处理复杂计算

Web Workers 允许在后台线程中运行脚本,不会阻塞主线程。适合处理大量数据计算、图像处理等CPU密集型任务。

示例:使用Web Worker进行费时的计算

javascript 复制代码
// 主线程代码 main.js
const worker = new Worker('worker.js');

// 向Worker发送数据
worker.postMessage({ data: largeArray });

// 接收Worker返回的结果
worker.onmessage = function(e) {
  console.log('计算结果:', e.data.result);
  // 更新UI
};

worker.onerror = function(error) {
  console.error('Worker错误:', error);
};

// Worker代码 worker.js
self.onmessage = function(e) {
  const data = e.data.data;
  // 执行复杂的计算(不会阻塞主线程!)
  const result = complexCalculation(data);
  
  // 将结果发送回主线程
  self.postMessage({ result: result });
};

function complexCalculation(data) {
  // 模拟复杂计算
  return data.map(item => item * 2).filter(item => item > 1000);
}

5. 事件委托(Event Delegation)

利用事件冒泡机制,将事件监听器绑定在父元素上,而不是每个子元素上。

示例:动态列表的事件处理

javascript 复制代码
// 不好的做法:为每个列表项绑定监听器
document.querySelectorAll('.item').forEach(item => {
  item.addEventListener('click', function() {
    console.log('点击了:', this.textContent);
  });
});

// 优化:使用事件委托,只需一个监听器
document.getElementById('list').addEventListener('click', function(e) {
  // 检查点击的是否是.item元素
  if (e.target && e.target.matches('.item')) {
    console.log('点击了:', e.target.textContent);
  }
});

// 优点:
// 1. 内存占用更少(只有一个监听器)
// 2. 对动态添加的子元素自动生效(无需重新绑定)
// 3. 初始化和销毁更高效

6. 代码层面优化

缓存查询结果和计算值

javascript 复制代码
// 不好的做法:每次循环都查询DOM和计算长度
for (let i = 0; i < document.querySelectorAll('.items').length; i++) {
  // ...
}

// 优化:缓存查询结果和长度
const items = document.querySelectorAll('.items');
const length = items.length;
for (let i = 0; i < length; i++) {
  // ...
}

使用更高效的选择器

javascript

dart 复制代码
// 较慢:需要遍历更多元素
document.querySelector('div .item .name');

// 较快:ID选择器最快
document.getElementById('myElement');

// 较快:类选择器配合querySelectorAll
document.querySelectorAll('.items');

避免在循环中修改DOM

javascript 复制代码
// 不好的做法:每次循环都修改DOM(导致多次回流)
const list = document.getElementById('list');
data.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item.name;
  list.appendChild(li); // 每次append都会可能引发回流
});

// 优化:使用DocumentFragment批量操作
const list = document.getElementById('list');
const fragment = document.createDocumentFragment();

data.forEach(item => {
  const li = document.createElement('li');
  li.textContent = item.name;
  fragment.appendChild(li);
});

list.appendChild(fragment); // 只引发一次回流

7. 使用性能API进行监控

利用 Performance API 来测量代码执行时间,找到性能瓶颈。

示例:测量函数执行时间

javascript 复制代码
// 使用 Performance Mark 和 Measure
function measurePerf() {
  // 开始标记
  performance.mark('start-processing');
  
  // 执行需要测量的代码
  heavyProcessing();
  
  // 结束标记
  performance.mark('end-processing');
  
  // 测量两个标记之间的时间
  performance.measure('processing-duration', 'start-processing', 'end-processing');
  
  // 获取测量结果
  const measures = performance.getEntriesByName('processing-duration');
  console.log(`处理耗时: ${measures[0].duration} 毫秒`);
  
  // 清理标记
  performance.clearMarks();
  performance.clearMeasures();
}

总结 :JavaScript执行优化的核心思想是 "减少主线程负担,避免长时间阻塞" 。通过任务分解、控制执行频率、利用后台线程、优化事件处理和代码逻辑,可以显著提升应用的响应速度和用户体验。

相关推荐
前端大卫2 分钟前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘18 分钟前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare19 分钟前
浅浅看一下设计模式
前端
Lee川22 分钟前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix1 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人1 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl1 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人1 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼1 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端