摘要: JavaScript是单线程的,低效的JavaScript代码会阻塞主线程,导致页面卡顿和无响应。本篇聚焦于编写高效的JavaScript,让主线程快速"空闲"出来,确保交互的流畅性。
核心内容:
1. 避免长任务(Long Tasks)与任务分解
任何在主线程上连续执行超过 50毫秒 的任务都被视为"长任务",它会阻塞用户交互(如点击、滚动)的响应。
优化策略:任务分解 。将一个大任务拆分成多个小任务,通过 setTimeout
、setInterval
或 requestIdleCallback
分片执行。
示例:处理一个大型数组
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执行优化的核心思想是 "减少主线程负担,避免长时间阻塞" 。通过任务分解、控制执行频率、利用后台线程、优化事件处理和代码逻辑,可以显著提升应用的响应速度和用户体验。