使用requestIdleCallback和requestAnimationFrame优化前端性能

使用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来实现更复杂的性能优化。例如:

  1. 使用requestAnimationFrame执行与渲染相关的高优先级任务
  2. 使用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或直接执行
  • 使用requestIdleCallbacktimeout选项确保任务最终会被执行
javascript 复制代码
// 设置timeout确保任务在2秒内执行
requestIdleCallback(handleLowPriorityTask, { timeout: 2000 });

3. 性能监控

  • 使用浏览器开发者工具(如Chrome DevTools的Performance面板)监控页面性能
  • 分析帧率(FPS)和主线程阻塞情况
  • 识别需要优化的任务

4. 避免滥用

  • 不要将所有任务都放入requestAnimationFramerequestIdleCallback
  • 只有在确实需要优化性能时才使用这些API
  • 对于简单任务,直接执行可能更高效

📝 总结

requestAnimationFramerequestIdleCallback是浏览器提供的两个强大的性能优化API,它们可以帮助我们更合理地安排任务执行时间,避免阻塞主线程。

requestAnimationFrame

  • 用于高优先级、与渲染相关的任务
  • 在下一次浏览器重绘之前执行
  • 适用于动画效果和需要与渲染同步的任务

requestIdleCallback

  • 用于低优先级、非紧急任务
  • 在浏览器空闲时间执行
  • 适用于批量处理和后台任务

通过合理使用这两个API,我们可以显著提升页面的流畅度和响应速度,为用户提供更好的体验。

希望这个小技巧对你有所帮助!如果你有任何问题或建议,欢迎在评论区留言讨论 🤗


相关资源:

标签: #前端性能优化 #requestAnimationFrame #requestIdleCallback #JavaScript性能 #React性能优化

相关推荐
dorisrv1 小时前
CSS Grid + Flexbox响应式复杂布局实现
前端
前端灵派派1 小时前
openlayer选择移动图标
前端
重铸码农荣光1 小时前
深入理解 JavaScript 继承:从原型链到 call/apply 的灵活运用
前端·javascript·面试
禅思院1 小时前
vite项目hmr热更新问题
前端·vue.js·架构
dorisrv1 小时前
TRAE SOLO 正式版:AI全链路开发的新范式 🚀
前端·trae
小明记账簿_微信小程序1 小时前
antd v3 select自定义下拉框内容失去焦点时会关闭下拉框
前端
前端老宋Running1 小时前
告别“祖传”defineProperty!Vue 3 靠 Proxy 练就了什么“神功”?
前端·vue.js·面试
码途进化论1 小时前
前端Docker多平台构建自动化实践
前端·javascript·后端
dorisrv1 小时前
React轻量级状态管理方案(useReducer + Context API)
前端