掌握 React 性能:Web Worker 和 Generator 函数

在构建数据密集型 React 应用程序时,您可能会遇到处理大型数据集导致 UI 冻结的情况。这是因为 JavaScript 在单个线程上运行,这意味着大量计算可能会阻止用户交互。让我们通过一个真实示例探索如何使用 Generator Functions 和 Web Workers 解决这个问题。

问题:繁重计算时 UI 冻结

假设您正在构建一个事件分析仪表板,需要处理数千个事件并进行复杂的计算。通常会发生以下情况:

function EventsDashboard() {
  const [events, setEvents] = useState([]);

  // This function blocks the UI thread
  const processEvents = (rawEvents) => {
    return rawEvents.map(event => {
      // Complex calculations that take time
      const score = calculateEventScore(event);  // ~2ms per event
      const sentiment = analyzeSentiment(event); // ~3ms per event
      const category = classifyEvent(event);     // ~1ms per event

      // With 10,000 events, this takes: 
      // 10,000 * (2 + 3 + 1) = 60,000ms = 60 seconds!
      return {
        ...event,
        score,
        sentiment,
        category
      };
    });
  };

  const processAndDisplay = () => {
    const processedEvents = processEvents(events);
    setEvents(processedEvents);
  };

  return (
    
); }

问题是什么?如果有 10,000 个事件,您的 UI 会冻结 60 秒!在此期间:

  • 用户无法点击按钮
  • 滚动不顺畅
  • 输入字段无响应
  • 动画冻结
  • 解决方案 1:分块处理的生成器函数

    生成器函数允许我们分块处理数据,并定期将控制权交还给主线程:

    function* eventProcessor(events, chunkSize = 100) {
      // Process events in chunks of 100
      for (let i = 0; i < events.length; i += chunkSize) {
        const chunk = events.slice(i, i + chunkSize);
    
        const processedChunk = chunk.map(event => ({
          ...event,
          score: calculateEventScore(event),
          sentiment: analyzeSentiment(event),
          category: classifyEvent(event)
        }));
    
        // Yield each processed chunk
        yield processedChunk;
      }
    }
    
    function EventsDashboard() {
      const [events, setEvents] = useState([]);
      const [progress, setProgress] = useState(0);
      const [isProcessing, setIsProcessing] = useState(false);
    
      const processEventsInChunks = async () => {
        setIsProcessing(true);
        const generator = eventProcessor(events);
        let processedEvents = [];
    
        try {
          while (true) {
            const { value: chunk, done } = generator.next();
            if (done) break;
    
            processedEvents = [...processedEvents, ...chunk];
    
            // Update progress
            const progress = (processedEvents.length / events.length) * 100;
            setProgress(progress);
    
            // Let the UI breathe
            await new Promise(resolve => setTimeout(resolve, 0));
    
            // Update UI with processed events so far
            setEvents(processedEvents);
          }
        } finally {
          setIsProcessing(false);
        }
      };
    
      return (
        
    {isProcessing && ( )}
    ); }

    解决方案 2:实现真正并行处理的 Web Worker

    Web Workers 允许我们在单独的线程中运行计算:

    // eventWorker.ts
    type Event = {
      id: string;
      name: string;
      timestamp: number;
      data: any;
    };
    
    type ProcessedEvent = Event & {
      score: number;
      sentiment: string;
      category: string;
    };
    
    type WorkerMessage = {
      type: 'PROCESS_CHUNK';
      payload: Event[];
    };
    
    type WorkerResponse = {
      type: 'CHUNK_PROCESSED' | 'PROCESSING_COMPLETE' | 'ERROR';
      payload: ProcessedEvent[] | Error;
      progress?: number;
    };
    
    self.onmessage = (e: MessageEvent) => {
      const { type, payload: events } = e.data;
    
      if (type === 'PROCESS_CHUNK') {
        try {
          let processedCount = 0;
          const totalEvents = events.length;
          const chunkSize = 100;
    
          // Process in smaller chunks to report progress
          for (let i = 0; i < events.length; i += chunkSize) {
            const chunk = events.slice(i, i + chunkSize);
    
            const processedChunk = chunk.map(event => ({
              ...event,
              score: calculateEventScore(event),
              sentiment: analyzeSentiment(event),
              category: classifyEvent(event)
            }));
    
            processedCount += chunk.length;
    
            // Report progress
            self.postMessage({
              type: 'CHUNK_PROCESSED',
              payload: processedChunk,
              progress: (processedCount / totalEvents) * 100
            });
          }
    
          self.postMessage({
            type: 'PROCESSING_COMPLETE',
            payload: events
          });
        } catch (error) {
          self.postMessage({
            type: 'ERROR',
            payload: error
          });
        }
      }
    };
    // EventsDashboard.tsx
    function EventsDashboard() {
      const [events, setEvents] = useState([]);
      const [progress, setProgress] = useState(0);
      const [error, setError] = useState(null);
      const workerRef = useRef();
    
      useEffect(() => {
        // Initialize worker
        workerRef.current = new Worker('/eventWorker.ts');
    
        // Handle worker messages
        workerRef.current.onmessage = (e) => {
          const { type, payload, progress } = e.data;
    
          switch (type) {
            case 'CHUNK_PROCESSED':
              setEvents(current => [...current, ...payload]);
              setProgress(progress);
              break;
    
            case 'PROCESSING_COMPLETE':
              setProgress(100);
              break;
    
            case 'ERROR':
              setError(payload);
              break;
          }
        };
    
        return () => workerRef.current?.terminate();
      }, []);
    
      const processEvents = () => {
        setEvents([]);
        setProgress(0);
        setError(null);
    
        workerRef.current.postMessage({
          type: 'PROCESS_CHUNK',
          payload: events
        });
      };
    
      return (
        
    {progress > 0 && progress < 100 && (
    )} {error && (
    Error: {error.message}
    )} 0 && progress < 100} />
    ); }

    最终解决方案:结合两种方法

    为了获得最佳性能,特别是对于非常大的数据集(100,000+ 个事件),请结合使用这两种方法:

  • 使用 Web Worker 进行并行处理
  • 使用 worker 内部的 Generator 函数进行分块处理
  • 将结果流回主线程
  • 完整的实现如下:

    // advancedEventWorker.ts
    function* processInChunks(events: Event[], chunkSize: number) {
      for (let i = 0; i < events.length; i += chunkSize) {
        const chunk = events.slice(i, i + chunkSize);
        yield chunk;
      }
    }
    
    self.onmessage = async (e: MessageEvent) => {
      const { type, payload: events } = e.data;
    
      if (type === 'PROCESS_EVENTS') {
        try {
          const CHUNK_SIZE = 100;
          const chunks = processInChunks(events, CHUNK_SIZE);
          let processedCount = 0;
          const totalEvents = events.length;
    
          for (const chunk of chunks) {
            // Process each chunk
            const processedChunk = await Promise.all(
              chunk.map(async event => ({
                ...event,
                score: await calculateEventScore(event),
                sentiment: await analyzeSentiment(event),
                category: await classifyEvent(event)
              }))
            );
    
            processedCount += chunk.length;
    
            // Stream results back to main thread
            self.postMessage({
              type: 'CHUNK_PROCESSED',
              payload: processedChunk,
              progress: (processedCount / totalEvents) * 100
            });
    
            // Simulate giving the worker thread a breather
            await new Promise(resolve => setTimeout(resolve, 0));
          }
    
          self.postMessage({
            type: 'PROCESSING_COMPLETE',
            payload: null,
            progress: 100
          });
        } catch (error) {
          self.postMessage({
            type: 'ERROR',
            payload: error
          });
        }
      }
    };

    性能监控

    为了衡量这些优化的影响:

    // Before processing
    performance.mark('processStart');
    
    // After processing
    performance.mark('processEnd');
    performance.measure(
      'eventProcessing',
      'processStart',
      'processEnd'
    );
    
    // Log metrics
    const metrics = performance.getEntriesByName('eventProcessing')[0];
    console.log(`Processing took ${metrics.duration}ms`);

    最佳实践和技巧

  • 选择正确的块大小:太小:频繁更新的开销太大:UI 变得无响应从每个块 100 个项目开始,并根据性能指标进行调整
  • 内存管理:
  • // Clear processed chunks from memory
    let processedEvents = new Array(totalEvents);
    for (const [index, chunk] of chunks.entries()) {
      processedEvents.splice(index * CHUNK_SIZE, CHUNK_SIZE, ...processedChunk);
      // Clear references to help garbage collection
      chunk.length = 0;
    }
  • 错误处理:
  • const safeProcess = async (event) => {
      try {
        return await processEvent(event);
      } catch (error) {
        console.error(`Failed to process event ${event.id}:`, error);
        return {
          ...event,
          error: error.message
        };
      }
    };
  • 消除:
  • function EventsDashboard() {
      const cancelRef = useRef(false);
    
      useEffect(() => {
        return () => {
          cancelRef.current = true;
        };
      }, []);
    
      const processEvents = async () => {
        for (const chunk of chunks) {
          if (cancelRef.current) break;
          // Process chunk...
        }
      };
    }

    实际性能改进

    通过此实现:

  • 处理 10,000 个事件:60 秒 → 3 秒
  • UI 始终保持响应
  • 内存使用量保持一致
  • 用户看到渐进式更新
  • 可以取消处理
  • 结论

    通过结合使用生成器函数和 Web Workers,我们可以处理密集的数据处理任务,同时保持流畅的用户体验。此模式对于以下情况特别有价值:

  • 数据可视化应用
  • 实时分析仪表板
  • 大数据集处理
  • 复杂计算
  • 文件处理应用程序
  • 关键是将大任务分解为可管理的部分,并以不阻塞主线程的方式处理它们,同时让用户了解进度。

    请记住在实施这些优化之前和之后始终测量性能,以确保它们为您的特定用例提供有意义的好处。