使用EventSource向Redux存数据时页面冻结问题排查与解决
差异原因分析
- EventSource的同步阻塞特性:EventSource的
onmessage回调是直接在主线程同步执行的。当服务器每秒推送10条消息时,这些回调会几乎无间隔地抢占主线程,每次触发两次Redux dispatch。随着store中数据量增长,reducer计算、组件重渲染的开销持续累加,主线程被完全占用,无法及时处理UI渲染、用户交互等任务,最终导致页面冻结。 - 异步调度的间隙差异:
setInterval+fetch模式中,fetch的响应回调属于微任务,浏览器会在当前宏任务结束后、下一次UI渲染前处理微任务;且setInterval的100ms间隔会给主线程留出喘息时间,让浏览器能在两次请求间隙完成渲染和其他任务。而EventSource的消息是独立宏任务,连续推送的消息会让主线程被dispatch和重渲染占满,没有间隙处理UI。 - 高频单条更新的重渲染开销:每次单条数据的dispatch都会触发订阅组件重渲染。EventSource每秒10次的高频单条更新,会导致组件每秒重渲染10次,随着数据量增大,每次重渲染的DOM对比、更新成本越来越高;而
setInterval的间隔让浏览器有机会合并部分渲染任务,所以能支撑更大的数据量。
解决方案
1. 批量处理EventSource消息
短时间内收集推送的消息,再一次性dispatch到Redux,减少dispatch次数和重渲染频率:
useEffect(() => { const eventSource = new EventSource('http://localhost:3000/events'); let messageBatch = []; // 每200ms批量处理一次,可根据业务调整间隔 const batchTimer = setInterval(() => { if (messageBatch.length === 0) return; // 批量分发数据 dispatch(addMapDataBatch( messageBatch.map(item => ({ DefectX: item.DefectPosX, DefectY: item.DefectPosY, DefectArea: item.DefectArea, })) )); dispatch(changeDefectListBatch(messageBatch)); messageBatch = []; }, 200); eventSource.onmessage = (event) => { const newMessage = JSON.parse(event.data); messageBatch.push(newMessage); }; eventSource.onerror = (err) => { console.error('EventSource failed:', err); clearInterval(batchTimer); eventSource.close(); }; return () => { clearInterval(batchTimer); eventSource.close(); }; }, []);
需对应在Redux reducer中新增批量处理的action,一次性处理数组数据,避免逐个处理的额外开销。
2. 优化组件重渲染
- 用
React.memo包裹订阅Redux store的组件,避免props未变化时的无意义重渲染; - 在组件内部用
useMemo缓存基于store数据生成的DOM列表等计算结果; - 用
useCallback缓存传递给子组件的函数,避免因函数引用变化触发子组件重渲染。
3. 限制store数据量
如果不需要保留全量历史数据,可设置数据上限,采用滚动窗口淘汰旧数据,减少每次处理的数据规模:
// 在changeDefectListBatch的reducer逻辑中 case 'CHANGE_DEFECT_LIST_BATCH': return { ...state, // 只保留最新10000条数据,可按需调整 defectList: [...state.defectList, ...action.payload].slice(-10000) };
4. 合并单次消息中的多dispatch
使用Redux的batchAPI(Redux Toolkit默认启用,手动使用需导入),将单条消息中的两次dispatch合并为一次更新,减少重渲染次数:
import { batch } from 'react-redux'; // 修改EventSource的onmessage回调 eventSource.onmessage = (event) => { const newMessage = JSON.parse(event.data); batch(() => { dispatch( addMapData({ DefectX: newMessage.DefectPosX, DefectY: newMessage.DefectPosY, DefectArea: newMessage.DefectArea, }) ); dispatch(changeDefectList(newMessage)); }); };
内容的提问来源于stack exchange,提问作者Ali KARABAY
相关产品推荐
相关产品推荐

