使用requestIdleCallback和requestAnimationFrame优化前端性能
🤔 为什么需要性能优化API?
在前端开发中,我们经常需要处理各种任务:DOM渲染、数据计算、网络请求等。如果这些任务处理不当,就会导致页面卡顿、响应缓慢,影响用户体验。
想象一下:用户正在滚动页面,同时我们的代码正在执行一个复杂的计算任务。这可能会导致页面卡顿,因为JavaScript是单线程的,复杂计算会阻塞主线程。
浏览器提供了两个强大的API来帮助我们优化这种情况:
requestAnimationFrame:在下一次浏览器重绘之前执行任务requestIdleCallback:在浏览器空闲时间执行任务
这两个API可以帮助我们更合理地安排任务执行时间,避免阻塞主线程,提升页面流畅度。
💡 基础概念
1. requestAnimationFrame
requestAnimationFrame是浏览器提供的一个API,它允许我们在下一次浏览器重绘之前执行回调函数。浏览器通常以60fps(每秒60次)的频率进行重绘,所以requestAnimationFrame的回调大约每16.7ms执行一次。
应用场景:
- 动画效果(如平滑滚动、进度条动画)
- 需要与页面渲染同步的任务
- 避免不必要的重绘和回流
2. requestIdleCallback
requestIdleCallback是浏览器提供的另一个API,它允许我们在浏览器空闲时间执行回调函数。浏览器空闲时间是指:
- 没有更高优先级的任务需要执行
- 一帧的渲染工作已经完成,还有剩余时间
应用场景:
- 低优先级任务(如日志记录、数据上报)
- 批量处理大量数据
- 预加载非关键资源
- 避免影响用户交互的任务
🚀 基础实现
1. requestAnimationFrame 示例:平滑滚动
javascript
// 平滑滚动到页面顶部
function smoothScrollToTop() {
const startPosition = window.pageYOffset;
const targetPosition = 0;
const distance = targetPosition - startPosition;
const duration = 800; // 动画持续时间(毫秒)
let startTime = null;
// 动画函数
function animation(currentTime) {
if (startTime === null) startTime = currentTime;
const timeElapsed = currentTime - startTime;
const progress = Math.min(timeElapsed / duration, 1);
// 使用缓动函数让动画更自然
const easeInOutQuad = progress < 0.5 ? 2 * progress * progress : -1 + (4 - 2 * progress) * progress;
window.scrollTo(0, startPosition + distance * easeInOutQuad);
if (timeElapsed < duration) {
requestAnimationFrame(animation);
}
}
requestAnimationFrame(animation);
}
// 使用示例
document.getElementById('scrollToTopBtn').addEventListener('click', smoothScrollToTop);
2. requestIdleCallback 示例:批量处理数据
javascript
// 需要处理的大量数据
const largeData = Array.from({ length: 10000 }, (_, i) => i + 1);
const batchSize = 100; // 每批处理的数据量
let index = 0;
// 处理单个数据项的函数
function processItem(item) {
// 模拟复杂处理逻辑
console.log(`处理数据项: ${item}`);
return item * 2;
}
// 使用requestIdleCallback批量处理数据
function processDataInBatches(deadline) {
// 只要浏览器有空闲时间且还有数据需要处理
while (deadline.timeRemaining() > 0 && index < largeData.length) {
processItem(largeData[index]);
index++;
// 每处理batchSize个数据项,检查一次时间
if (index % batchSize === 0) {
console.log(`已处理 ${index} 个数据项`);
break;
}
}
// 如果还有数据需要处理,继续请求空闲时间
if (index < largeData.length) {
requestIdleCallback(processDataInBatches);
} else {
console.log('所有数据处理完成');
}
}
// 启动批量处理
requestIdleCallback(processDataInBatches);
🎯 进阶实现:组合使用两个API
在实际项目中,我们常常需要组合使用这两个API来实现更复杂的性能优化。例如:
- 使用
requestAnimationFrame执行与渲染相关的高优先级任务 - 使用
requestIdleCallback执行低优先级的后台任务
示例:高性能列表渲染
javascript
class HighPerformanceList {
constructor(container, items, itemRenderer) {
this.container = container;
this.items = items;
this.itemRenderer = itemRenderer;
this.renderedItems = new Set();
this.batchSize = 20; // 每批渲染的项目数
this.currentIndex = 0;
this.init();
}
init() {
// 首先渲染可见区域的项目(高优先级)
this.renderVisibleItems();
// 然后在空闲时间渲染剩余项目(低优先级)
this.renderRemainingItems();
}
renderVisibleItems() {
const containerRect = this.container.getBoundingClientRect();
const visibleItems = this.items.filter(item => {
// 简化的可见性检查
const itemRect = item.rect || { top: 0, bottom: 0 };
return itemRect.bottom >= containerRect.top && itemRect.top <= containerRect.bottom;
});
// 使用requestAnimationFrame渲染可见项目
requestAnimationFrame(() => {
visibleItems.forEach(item => {
if (!this.renderedItems.has(item.id)) {
this.renderItem(item);
}
});
});
}
renderRemainingItems() {
// 使用requestIdleCallback在空闲时间渲染剩余项目
requestIdleCallback((deadline) => {
let renderedCount = 0;
while (deadline.timeRemaining() > 0 && this.currentIndex < this.items.length) {
const item = this.items[this.currentIndex];
if (!this.renderedItems.has(item.id)) {
this.renderItem(item);
renderedCount++;
// 每渲染batchSize个项目,检查一次时间
if (renderedCount >= this.batchSize) {
break;
}
}
this.currentIndex++;
}
// 如果还有项目需要渲染,继续请求空闲时间
if (this.currentIndex < this.items.length) {
this.renderRemainingItems();
}
});
}
renderItem(item) {
const itemElement = this.itemRenderer(item);
this.container.appendChild(itemElement);
this.renderedItems.add(item.id);
}
}
// 使用示例
const container = document.getElementById('list-container');
const items = Array.from({ length: 1000 }, (_, i) => ({
id: i,
content: `项目 ${i + 1}`,
rect: { top: i * 50, bottom: (i + 1) * 50 }
}));
function itemRenderer(item) {
const div = document.createElement('div');
div.className = 'list-item';
div.textContent = item.content;
return div;
}
new HighPerformanceList(container, items, itemRenderer);
🎨 React 中的应用
1. 使用 requestAnimationFrame 实现动画效果
javascript
import React, { useRef, useEffect, useState } from 'react';
const SmoothProgressBar = ({ progress }) => {
const [displayProgress, setDisplayProgress] = useState(0);
const ref = useRef(null);
useEffect(() => {
let animationId;
const startValue = displayProgress;
const targetValue = progress;
const startTime = performance.now();
const duration = 500; // 动画持续时间(毫秒)
const updateProgress = (currentTime) => {
const elapsedTime = currentTime - startTime;
const progress = Math.min(elapsedTime / duration, 1);
// 使用缓动函数
const easeInOut = progress < 0.5
? 4 * progress * progress * progress
: (progress - 1) * (2 * progress - 2) * (2 * progress - 2) + 1;
const currentProgress = startValue + (targetValue - startValue) * easeInOut;
setDisplayProgress(currentProgress);
if (progress < 1) {
animationId = requestAnimationFrame(updateProgress);
}
};
animationId = requestAnimationFrame(updateProgress);
return () => {
if (animationId) {
cancelAnimationFrame(animationId);
}
};
}, [progress]);
return (
<div className="progress-bar-container">
<div
ref={ref}
className="progress-bar-fill"
style={{ width: `${displayProgress}%` }}
/>
<span className="progress-text">{Math.round(displayProgress)}%</span>
</div>
);
};
// 使用示例
function App() {
const [progress, setProgress] = useState(0);
const handleButtonClick = () => {
setProgress(prev => Math.min(prev + 20, 100));
};
return (
<div className="app">
<h1>平滑进度条</h1>
<SmoothProgressBar progress={progress} />
<button onClick={handleButtonClick}>增加进度</button>
</div>
);
}
2. 使用 requestIdleCallback 优化数据处理
javascript
import React, { useEffect, useState } from 'react';
const LargeDataProcessor = () => {
const [processedCount, setProcessedCount] = useState(0);
const [status, setStatus] = useState('idle');
useEffect(() => {
// 模拟大量数据
const largeData = Array.from({ length: 5000 }, (_, i) => i + 1);
let processedIndex = 0;
const processBatch = (deadline) => {
// 只要浏览器有空闲时间且还有数据需要处理
while (deadline.timeRemaining() > 0 && processedIndex < largeData.length) {
// 模拟复杂的数据处理
const data = largeData[processedIndex];
const result = data * 2 + Math.sqrt(data);
console.log(`处理数据 ${data},结果:${result}`);
processedIndex++;
setProcessedCount(processedIndex);
}
// 如果还有数据需要处理,继续请求空闲时间
if (processedIndex < largeData.length) {
requestIdleCallback(processBatch);
} else {
setStatus('completed');
}
};
// 启动数据处理
setStatus('processing');
requestIdleCallback(processBatch);
}, []);
return (
<div className="data-processor">
<h2>数据处理状态</h2>
<p>状态:{status}</p>
<p>已处理数据:{processedCount}/5000</p>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${(processedCount / 5000) * 100}%` }}
/>
</div>
</div>
);
};
⚠️ 注意事项
1. 浏览器兼容性
requestAnimationFrame:支持所有现代浏览器,IE10+可用requestIdleCallback:支持Chrome 47+、Firefox 55+、Edge 79+,Safari不支持
解决方案:使用polyfill
javascript
// requestIdleCallback polyfill
if (!window.requestIdleCallback) {
window.requestIdleCallback = function(callback, options) {
const timeout = options && options.timeout || 1000;
const start = Date.now();
return setTimeout(function() {
callback({
didTimeout: false,
timeRemaining: function() {
return Math.max(0, timeout - (Date.now() - start));
}
});
}, 1);
};
window.cancelIdleCallback = function(id) {
clearTimeout(id);
};
}
2. 优先级管理
- 不要在
requestIdleCallback中执行高优先级任务(如用户交互、动画) - 对于紧急任务,使用
requestAnimationFrame或直接执行 - 使用
requestIdleCallback的timeout选项确保任务最终会被执行
javascript
// 设置timeout确保任务在2秒内执行
requestIdleCallback(handleLowPriorityTask, { timeout: 2000 });
3. 性能监控
- 使用浏览器开发者工具(如Chrome DevTools的Performance面板)监控页面性能
- 分析帧率(FPS)和主线程阻塞情况
- 识别需要优化的任务
4. 避免滥用
- 不要将所有任务都放入
requestAnimationFrame或requestIdleCallback中 - 只有在确实需要优化性能时才使用这些API
- 对于简单任务,直接执行可能更高效
📝 总结
requestAnimationFrame和requestIdleCallback是浏览器提供的两个强大的性能优化API,它们可以帮助我们更合理地安排任务执行时间,避免阻塞主线程。
requestAnimationFrame:
- 用于高优先级、与渲染相关的任务
- 在下一次浏览器重绘之前执行
- 适用于动画效果和需要与渲染同步的任务
requestIdleCallback:
- 用于低优先级、非紧急任务
- 在浏览器空闲时间执行
- 适用于批量处理和后台任务
通过合理使用这两个API,我们可以显著提升页面的流畅度和响应速度,为用户提供更好的体验。
希望这个小技巧对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言讨论 🤗
相关资源:
- MDN - requestAnimationFrame
- MDN - requestIdleCallback
- Web.dev - 优化动画性能
- Google Developers - 使用requestIdleCallback
标签: #前端性能优化 #requestAnimationFrame #requestIdleCallback #JavaScript性能 #React性能优化