Next.js与React.js中Video.js加载大量视频时应用崩溃求助
解决大量Video.js播放器导致的应用崩溃问题
当一次性渲染120-140个Video.js播放器时,应用崩溃的核心原因是内存占用过高(过多DOM节点、播放器实例、事件监听器)以及不必要的资源加载。以下是针对性的优化方案:
1. 实现虚拟滚动,只渲染可视区域内的播放器
一次性渲染所有播放器会生成大量DOM节点,直接撑爆内存。使用虚拟滚动库(如react-window)只渲染当前视口内的播放器,其余区域用占位符代替,大幅减少DOM数量。
示例改造思路:
// 父组件中使用FixedSizeList import { FixedSizeList as List } from 'react-window'; const VideoList = ({ videos }) => { const renderPlayer = ({ index, style }) => { const video = videos[index]; return ( <div style={style}> <LukPlayer options={video.options} data={video.data} // 传递其他所需props /> </div> ); }; return ( <List height={window.innerHeight} itemCount={videos.length} itemSize={300} // 每个播放器容器的固定高度 width="100%" > {renderPlayer} </List> ); };
2. 动态创建/销毁播放器实例,避免内存泄漏
当前组件在卸载时会调用dispose,但如果播放器不在可视区域,应该主动销毁实例、清空DOM,只保留占位元素;进入视口时再重新初始化播放器,释放闲置资源。
修改LukPlayer核心逻辑:
const LukPlayer = memo((props) => { const videoRef = useRef(null); const playerRef = useRef(null); const observerRef = useRef(null); const { options, onReady, data, commentFlag, flag } = props; // ... 保留原有状态、工具函数 useEffect(() => { let isMounted = true; const videoNode = videoRef.current; const observerOptions = { root: null, rootMargin: '0px', threshold: 0.5, }; observerRef.current = new IntersectionObserver(async (entries) => { entries.forEach(async (entry) => { if (!isMounted) return; if (entry.isIntersecting) { // 进入视口,初始化播放器 if (!playerRef.current) { const videoElement = document.createElement('video-js'); videoElement.classList.add('vjs-big-play-centered'); videoRef.current.appendChild(videoElement); const validOptions = options?.sources?.[0]?.src?.includes('undefined') ? null : options; if (validOptions) { const player = videojs(videoElement, validOptions, () => onReady?.(player)); playerRef.current = player; player.on('ended', onVideoEnd); } } await playerRef.current?.play(); setCurrentPlaying(data); } else { // 离开视口,暂停并销毁播放器 if (playerRef.current) { playerRef.current.pause(); playerRef.current.dispose(); playerRef.current = null; // 清空DOM节点,释放内存 videoRef.current.innerHTML = ''; } } }); }, observerOptions); observerRef.current.observe(videoNode); return () => { isMounted = false; observerRef.current?.disconnect(); playerRef.current?.dispose(); playerRef.current = null; }; }, [commentFlag, flag === 'create' && options, data]); // ... 保留其他逻辑与返回结构 });
3. 共享全局IntersectionObserver实例
每个播放器创建独立的Observer会导致大量实例占用内存,改为全局共享一个Observer监听所有播放器元素,减少资源消耗。
在全局文件(如App.js)创建共享Observer:
// App.js export const videoObserver = new IntersectionObserver((entries) => { entries.forEach(async (entry) => { const { playerRef, videoData, videoOptions, setPlaying } = entry.target; if (entry.isIntersecting) { if (!playerRef.current) { const videoElement = document.createElement('video-js'); videoElement.classList.add('vjs-big-play-centered'); entry.target.appendChild(videoElement); const validOptions = videoOptions?.sources?.[0]?.src?.includes('undefined') ? null : videoOptions; if (validOptions) { const player = videojs(videoElement, validOptions); playerRef.current = player; player.on('ended', () => { /* 视频结束逻辑 */ }); } } await playerRef.current?.play(); setPlaying(videoData); } else { if (playerRef.current) { playerRef.current.pause(); playerRef.current.dispose(); playerRef.current = null; entry.target.innerHTML = ''; } } }); }, { root: null, rootMargin: '0px', threshold: 0.5 });
在LukPlayer中注册元素到全局Observer:
// LukPlayer.js import { videoObserver } from './App'; const LukPlayer = memo((props) => { const videoRef = useRef(null); const playerRef = useRef(null); const { options, data } = props; const [playing, setCurrentPlaying] = useState([]); useEffect(() => { const node = videoRef.current; // 绑定所需属性到DOM节点,供全局Observer调用 node.playerRef = playerRef; node.videoData = data; node.videoOptions = options; node.setPlaying = setCurrentPlaying; videoObserver.observe(node); return () => { videoObserver.unobserve(node); playerRef.current?.dispose(); playerRef.current = null; }; }, [options, data]); // ... 保留其他逻辑与返回结构 });
4. 优化全局事件监听
当前每个播放器都绑定keydown事件到document,会导致重复绑定。改为全局只绑定一次,通过全局状态判断当前播放的播放器:
// 在全局状态(如Redux或App组件)中维护当前播放的视频ID useEffect(() => { const handleKeyPress = async (event) => { if (event.code === 'Space') { event.preventDefault(); const currentPlayingId = store.getState().currentPlayingId; // 用全局ref集合存储所有播放器实例,通过ID获取 const player = playerInstances.current.get(currentPlayingId); if (player) { player.paused() ? await player.play() : player.pause(); } } }; document.addEventListener('keydown', handleKeyPress); return () => document.removeEventListener('keydown', handleKeyPress); }, []);
5. 其他细节优化
- 缓存iOS判断结果:用
useMemo缓存check_iOS的结果,避免每次渲染重复执行:const ios = useMemo(() => { if (!isClient) return false; return [ 'iPad Simulator', 'iPhone Simulator', 'iPod Simulator', 'iPad', 'iPhone', 'iPod' ].includes(navigator.platform) || /^((?!chrome|android).)*safari/i.test(navigator.userAgent); }, [isClient]); - 稳定props依赖:父组件传递
options时用useMemo包裹,确保对象引用稳定,避免memo失效导致不必要的重渲染:// 父组件中 const videoOptions = useMemo(() => ({ sources: [{ src: videoUrl, type: 'video/mp4' }], autoplay: true, // 其他配置项 }), [videoUrl]);
内容的提问来源于stack exchange,提问作者amin ronak
相关产品推荐
相关产品推荐

