Repository navigation
Description
Description of the bug
The Resolution class in postprocessing holds a resizable back-pointer to the parent pass that owns it (set in the constructor, used by setBaseSize to call resizable.setSize(...)). This creates a parent ↔ child cycle on every pass that constructs a Resolution — most notably KawaseBlurPass, which is used internally by BloomEffect. The cycle makes those effects unsafe to traverse with any standard graph-walking API:
- JSON.stringify — throws TypeError: Converting circular structure to JSON
- structuredClone — throws DataCloneError
- postMessage, devtools deep-inspect, and any consumer that hashes/serializes the effect graph
The most visible downstream impact is that @react-three/postprocessing is currently unusable under React 19 with reactStrictMode: true (the default for new Next.js 16+ projects). React 19's reconciler calls JSON.stringify on effect props during dev-mode prop diffing / error formatting, hits the cycle, and crashes the entire R3F tree.
The back-pointer is also functionally redundant: Resolution already extends EventDispatcher and dispatches a 'change' event from setBaseSize. Parents can subscribe to that event instead of being held by reference, removing the cycle without changing observable behavior.
To reproduce
Minimal repro (no React, no R3F, plain Node or browser console):
import { KawaseBlurPass } from 'postprocessing'
const pass = new KawaseBlurPass()
JSON.stringify(pass)
// TypeError: Converting circular structure to JSON
// --> starting at object with constructor 'KawaseBlurPass'
// | property 'resolution' -> object with constructor 'Resolution'
// --- property 'resizable' closes the circlestructuredClone(pass) throws DataCloneError for the same reason.
Real-world repro (Next.js 16 + React 19 + R3F 9 + the JSX wrapper):
next.config.mjs:
const nextConfig = { reactStrictMode: true }
export default nextConfigA client component:
'use client'
import { Canvas } from '@react-three/fiber'
import { EffectComposer, Bloom } from '@react-three/postprocessing'
export default function Scene() {
return (
<Canvas>
<mesh>
<boxGeometry />
<meshStandardMaterial />
</mesh>
<EffectComposer>
<Bloom intensity={0.5} luminanceThreshold={0.9} mipmapBlur />
</EffectComposer>
</Canvas>
)
}npm run dev and load the page. Console:
Uncaught TypeError: Converting circular structure to JSON
--> starting at object with constructor 'KawaseBlurPass'
| property 'resolution' -> object with constructor 'Resolution'
--- property 'resizable' closes the circle
at JSON.stringify (<anonymous>)
...
at ScenePost
at SceneDirector
at Canvas
The page never paints. Disabling reactStrictMode masks the symptom but doesn't fix the underlying cycle.
Expected behavior
JSON.stringify(pass) and structuredClone(pass) should not throw on any pass that uses a Resolution, and the @react-three/postprocessing JSX wrapper should mount cleanly under React 19 + StrictMode.
The proposed fix is to drop the resizable back-pointer from Resolution and have parents subscribe to the existing 'change' event:
// Resolution.ts
class Resolution extends EventDispatcher {
constructor(baseWidth = 1, baseHeight = 1, scale = 1) {
super()
this.baseWidth = baseWidth
this.baseHeight = baseHeight
this.scale = scale
// (no `this.resizable` field)
}
setBaseSize(width, height) {
if (this.baseWidth !== width || this.baseHeight !== height) {
this.baseWidth = width
this.baseHeight = height
this.dispatchEvent({ type: 'change' })
// (no `this.resizable.setSize(...)` call)
}
}
}// In every pass that owns a Resolution, e.g. KawaseBlurPass:
class KawaseBlurPass extends Pass {
constructor(/* ... */) {
super()
this.resolution = new Resolution(width, height, scale)
this.resolution.addEventListener('change', () => {
this.setSize(this.resolution.baseWidth, this.resolution.baseHeight)
})
}
}Same observable behavior, no cycle, no JSON.stringify trap, no structuredClone trap, no future-React reconciler trap. This is a breaking change for any consumer that constructs Resolution directly with the legacy (resizable, baseWidth, baseHeight, scale) signature, so it would land in a major version (or behind a deprecation pass that accepts both signatures). It's also worth ensuring the v7 RenderPipeline redesign in #419 keeps Resolution back-pointer-free for the same reasons.
Happy to PR this if maintainers agree on the approach.
Screenshots
N/A - bug is a console error, fully captured in the To reproduce section.
Library versions used
- Three: 0.183.2
- Post Processing: 6.39.0
(also reproduced via @react-three/[email protected], @react-three/[email protected], [email protected], [email protected])
Desktop
- OS: Windows 11
- Browser Chrome 131.0.6778.86
- Graphics hardware: NVIDIA
Mobile
N/A - bug reproduces on every platform that runs JavaScript. Not device-specific.
Activity
vanruesc commentedon Apr 9, 2026
Thanks for the report.
I think I'm becoming paranoid, but this reads a bit like a ChatGPT summary 😅
The Resolution.resizable property has been deprecated since 2022.
True, I'll just remove it in a backward-compatible way for the next release.
That's already the case. Thanks for the heads-up though 👍
Remove resizable
vanruesc commentedon Apr 17, 2026
Should be fixed in v6.39.1