掌握 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+ 个事件),请结合使用这两种方法:
完整的实现如下:
// 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`);
最佳实践和技巧
// 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... } }; }
实际性能改进
通过此实现:
结论
通过结合使用生成器函数和 Web Workers,我们可以处理密集的数据处理任务,同时保持流畅的用户体验。此模式对于以下情况特别有价值:
关键是将大任务分解为可管理的部分,并以不阻塞主线程的方式处理它们,同时让用户了解进度。
请记住在实施这些优化之前和之后始终测量性能,以确保它们为您的特定用例提供有意义的好处。