多页面使用双Canvas时WebGL上下文丢失错误的修复
解决Next.js中页面跳转时WebGL上下文丢失的问题
问题核心
在Next.js+TypeScript项目中,首页使用react-three/fiber的Canvas渲染3D模型,About页使用自定义WebGL Canvas实现交互效果,通过framer-motion的AnimatePresence做导航过渡时,出现错误:WebGL context was lost. loseContext: Already lost.,手动在useEffect返回函数中清理画布无效,对将Canvas移至Layout层的方案存在疑问。
错误原因分析
- 自定义WebGL组件的清理逻辑缺陷:当前代码中
gl变量疑似全局共享,清理时可能重复调用loseContext;且useEffect依赖为空数组,页面切换时不会触发重新清理,旧上下文未正确释放就创建新上下文。 - AnimatePresence的
wait模式延迟销毁:mode="wait"会等待退出动画完成后再跳转,旧页面的WebGL组件在过渡期间仍存在,新页面的Canvas创建时可能导致上下文冲突。 - 多页面独立Canvas的资源冲突:每个页面单独创建Canvas实例,浏览器对WebGL上下文的数量有限制,频繁创建销毁易导致上下文丢失。
解决方案
1. 修复自定义WebGL组件的清理逻辑
将gl变量封装在组件内部,通过useRef保存,同时添加路由依赖确保页面切换时触发清理:
import { useRef, useEffect } from 'react'; import { usePathname } from 'next/navigation'; const LogoDistort: React.FC<LogoDistortProps> = ({ logoPath }) => { const canvasRef = useRef<HTMLCanvasElement>(null); const glRef = useRef<WebGLRenderingContext | null>(null); const animationFrameIdRef = useRef<number | undefined>(undefined); const pathname = usePathname(); const particlesRef = useRef<any[]>([]); useEffect(() => { if (!canvasRef.current) return; const canvas = canvasRef.current; // 初始化WebGL上下文 const gl = canvas.getContext('webgl'); if (!gl) return; glRef.current = gl; particlesRef.current = []; setupWebGL(canvas, gl); // 修改setupWebGL,传入gl参数 setupShaders(gl); // 传入gl loadImg(canvas, logoPath, gl); // 传入gl setupEvents(canvas, logoPath, gl); // 传入gl return () => { // 正确清理顺序 if (animationFrameIdRef.current) { cancelAnimationFrame(animationFrameIdRef.current); animationFrameIdRef.current = undefined; } // 销毁WebGL资源(着色器、缓冲区等) if (glRef.current) { // 示例:销毁着色器程序(需根据实际代码调整) // if (programRef.current) glRef.current.deleteProgram(programRef.current); // 清理所有缓冲区、纹理等资源 const ext = glRef.current.getExtension("WEBGL_lose_context"); if (ext) { ext.loseContext(); } glRef.current = null; } particlesRef.current = []; }; }, [pathname, logoPath]); // 添加路由和logoPath作为依赖 return <canvas ref={canvasRef} className="canvas" />; }; export default LogoDistort;
2. 实现Canvas移至Layout层的方案
Next.js的Layout组件是全局共享的,将WebGL Canvas放在Layout中,根据当前路由切换渲染内容,避免频繁创建销毁上下文:
步骤1:创建全局WebGL容器组件
// components/GlobalWebGL.tsx 'use client'; import { usePathname } from 'next/navigation'; import EarthScene from './EarthScene'; // 首页的r3f场景组件 import LogoDistort from './LogoDistort'; // About页的自定义WebGL组件 const GlobalWebGL = () => { const pathname = usePathname(); return ( <div className="fixed inset-0 z-0"> {pathname === '/' && <EarthScene />} {pathname === '/about' && <LogoDistort logoPath="/your-logo.png" />} </div> ); }; export default GlobalWebGL;
步骤2:在Layout中引入全局WebGL组件
// app/layout.tsx import GlobalWebGL from '@/components/GlobalWebGL'; import Navbar from '@/components/Navbar'; export default function RootLayout({ children, }: { children: React.ReactNode; }) { return ( <html lang="zh-CN"> <body> <GlobalWebGL /> <Navbar /> <main className="relative z-10">{children}</main> </body> </html> ); }
这样所有页面共享同一个Canvas容器,路由切换时仅切换渲染的场景,从根源上避免上下文频繁创建销毁的问题。
3. 调整AnimatePresence的导航逻辑
确保旧页面的WebGL组件在跳转前完全销毁,修改导航按钮的点击逻辑,先关闭菜单再跳转:
// Navbar组件中的点击函数 const clickHome = () => { setMenuOpen(false); // 等待菜单退出动画完成后再跳转 setTimeout(() => { router.push('/'); }, 300); // 对应菜单exit动画的duration }; const clickAbout = () => { setMenuOpen(false); setTimeout(() => { router.push('/about'); }, 300); };
同时可移除AnimatePresence的mode="wait",避免延迟跳转导致的上下文冲突:
<AnimatePresence onExitComplete={() => { if (pendingRoute) { router.push(pendingRoute); setPendingRoute(null); } }} > {/* 菜单内容 */} </AnimatePresence>
关键注意事项
- 避免使用全局的WebGL上下文变量,每个组件实例应独立管理自己的上下文。
- 清理WebGL资源时,要先销毁着色器、缓冲区等资源,再释放上下文。
- 全局Canvas方案适合多页面包含WebGL内容的项目,能有效减少上下文资源冲突。
内容的提问来源于stack exchange,提问作者Athul Kesav
相关产品推荐
相关产品推荐

