修复 Next.js 开发环境中的 WebGL Context Lost(R3F + GSAP)
在本地开发 GoalsShowcase
(3D 行星 + 滚动叙事)时,我遇到了一个只在 dev 环境复现的问题:页面滚动到该区块时,控制台打印 THREE.WebGLRenderer: Context Lost.
,3D 区域变成白屏;但 build && start
生产环境一切正常。
本文记录了我的排查过程、根因分析、最终方案,以及可选的进一步加固建议,供后续复用。
症状与环境
-
症状
- 滚动到
GoalsShowcase
时白屏,并在控制台看到THREE.WebGLRenderer: Context Lost.
。 - 生产环境(
next build && next start
)不出现。
- 滚动到
-
相关代码路径
- 页面入口:
src/app/[locale]/page.tsx
- 3D 场景:
src/components/goals-showcase/GoalsScene.tsx
- 粘性滚动 + 叙事:
src/components/goals-showcase/GoalsShowcase.tsx
- 另一处 3D(已用于对比/排查):
src/components/ui/globe-section.tsx
,src/components/ui/globe.tsx
- 页面入口:
-
技术栈
- Next.js 15(App Router)
- React Three Fiber + three.js
- GSAP + ScrollTrigger(pin 固定、scrub、snap)
根因分析
结合社区建议与多次试验,dev 环境 Context Lost 的关键触发主要是以下组合:
-
React 18 StrictMode 的 dev 双重挂载/卸载 + Fast Refresh 重渲染
- 开发模式下,组件会被重复 mount/unmount 以帮你发现副作用问题,这对 WebGL Canvas 稳定性很不友好。
-
GSAP ScrollTrigger 的
pinReparent: true
- 为了让被 pin 的节点“逃离”祖先的
transform/overflow
影响,ScrollTrigger 会在 pin 时将节点临时重挂载到body
下(reparent)。 - 当被 pin 的节点里包含 WebGL Canvas,加上 StrictMode/HMR 的抖动,极易触发上下文丢失。
- 为了让被 pin 的节点“逃离”祖先的
-
较高的 DPR / GPU 压力
<Canvas dpr={[1, 1.8]}>
在高 DPI 显示器上 VRAM 占用明显上升,配合纹理/HDR 峰值,dev 的“反复挂载”更容易把上下文丢掉。
社区参考:R3F 维护者 drcmda 给出的建议是“尽量保持单一 Canvas,避免跨路由/区块频繁创建、销毁和重挂载 Canvas;Context Lost 并非总是致命,但要尽量减少触发”。
- three.js 论坛(drcmda):https://discourse.threejs.org/t/context-lost-when-i-route-to-another-page-in-react-three-fiber/61736
- Khronos WebGL 指南: https://www.khronos.org/webgl/wiki/HandlingContextLost
我尝试过的修复路线
以下是我为 dev 稳定性准备的几种可选加固(均不改变生产效果):
-
降低 DPR(仅 dev/移动端)
- 例如 dev 桌面
[1, 1.3~1.4]
,dev 移动[1, 1.1~1.15]
;prefers-reduced-motion
下固定[1, 1]
。
- 例如 dev 桌面
-
关闭
pinReparent
(仅 dev)pinReparent: process.env.NODE_ENV === 'development' ? false : true
- 避免 Canvas 在滚动 pin 时被重挂载。
-
添加 WebGL Context Lost 处理器
- 监听
webglcontextlost
并event.preventDefault()
,允许浏览器自动尝试恢复;监听webglcontextrestored
后可重启渲染循环。
- 监听
-
保守的渲染设置
- dev 下降低抗锯齿/后期;关闭
preserveDrawingBuffer
(会升高内存占用,通常不建议打开)。
- dev 下降低抗锯齿/后期;关闭
上述措施能显著降低 dev 中 Context Lost 的概率,并在丢失时实现优雅恢复。
最终解决方案(本次采用)
这次我选择了最小改动策略:
- 在
next.config.mjs
关闭 StrictMode(dev):
// next.config.mjs
const nextConfig = {
reactStrictMode: false,
// ...
};
export default withNextIntl(nextConfig);
关闭后,开发环境下 GoalsShowcase
立即恢复稳定,白屏/Context Lost 不再出现。
说明:生产环境可以继续保持最佳实践(比如较高 DPR、保留 pinReparent=true 等)。这次仅针对 dev 体验做最小化切换。
可选的进一步加固(按需采纳)
如果团队希望在 StrictMode 保持开启的前提下提升鲁棒性,可以考虑:
-
GoalsShowcase.tsx
(ScrollTrigger)- dev 环境
pinReparent: false
,并适度降低scrub
。
- dev 环境
-
GoalsScene.tsx
(R3F Canvas)- dev/移动端下调
dpr
;添加webglcontextlost/restored
监听与自动恢复逻辑。 - 必要时 dev 下
gl.antialias: false
、限制 HDR 和大纹理。
- dev/移动端下调
-
单 Canvas 策略
- 页面上尽量只保持一个可见的 WebGL Canvas;可用 IntersectionObserver 在区块进入/离开可视区域时挂载/卸载。
经验与教训
- dev ≠ prod:StrictMode 的双重挂载 + HMR 对 WebGL 是“放大镜”,很多生产环境不会暴露的问题会在 dev 爆出来。
- 减少 DOM reparent/重排:尤其是对含 WebGL 的节点;在 dev 里适度放宽(pinReparent 关闭)能提升稳定性。
- 控制内存与像素比:DPR、抗锯齿、贴图/HDR 都是 VRAM 的主要开销。
- 准备好上下文丢失兜底:
webglcontextlost
事件务必preventDefault()
,能从“致命白屏”变成“可恢复”。
参考链接
- R3F 维护者建议(three.js 论坛): https://discourse.threejs.org/t/context-lost-when-i-route-to-another-page-in-react-three-fiber/61736
- WebGL 官方:上下文丢失处理 https://www.khronos.org/webgl/wiki/HandlingContextLost
- three.js Tips:不要随意开启
preserveDrawingBuffer
https://discoverthreejs.com/tips-and-tricks/
—— 以上就是这次在 dev 环境修复 WebGL Context Lost 的完整记录。如果你也在使用 R3F + GSAP 做滚动叙事,欢迎参考并按需采纳加固建议。