[§] /docs/how-to/post-processing-stack
How to stack post-processing without jank
Add bloom, glitch, chromatic aberration and a custom CRT pass, and change them live without a single dropped frame.
TL;DR
- Mount every effect once. Turn it "off" by tweening its strength to zero.
- Order matters: bloom, then colour shifts, then CRT, then noise last.
- One
EffectComposerper canvas, even when dreiViews split it into many scenes.
Goal: a screen that can go from clean to "signal lost" and back, with no stutter when it switches.
You need
@react-three/postprocessing3 andpostprocessing6.- A scene already running in React Three Fiber.
scene ─► Bloom light first ─► ChromaticAberration colour split ─► Glitch cuts ─► CRT (custom) scanlines + curve ─► Noise grain, always last ─► screen
Steps
1. Mount all effects once
<EffectComposer multisampling={0}>
<Bloom mipmapBlur intensity={0.8} luminanceThreshold={0.6} resolutionScale={0.5} />
<ChromaticAberration ref={ca} offset={[0, 0]} />
<Glitch ref={glitch} active={false} />
<CRT ref={crt} />
<Noise opacity={0.06} premultiply />
</EffectComposer>2. Never mount or unmount an effect to switch it
Adding or removing an effect rebuilds the passes and recompiles shaders. That is a visible hitch of 100ms or more. Keep them mounted and change their strength instead.
3. Tween strength from the frame loop
useFrame(() => {
const s = p.current; // 0 clean, 1 full chaos
ca.current.offset.set(0.004 * s, 0.002 * s);
crt.current.uniforms.get('uStrength').value = s;
glitch.current.mode = s > 0.8 ? GlitchMode.CONSTANT_MILD : GlitchMode.DISABLED;
});4. Write a custom effect for the CRT look
import { Effect } from 'postprocessing';
import { Uniform } from 'three';
const frag = /* glsl */ `
uniform float uStrength;
void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
vec2 c = uv - 0.5;
vec2 bent = uv + c * dot(c, c) * 0.15 * uStrength; // screen curve
vec3 col = texture2D(inputBuffer, bent).rgb;
col *= 1.0 - 0.25 * uStrength * step(0.5, fract(bent.y * resolution.y * 0.5)); // scanlines
outputColor = vec4(col, inputColor.a);
}`;
export class CRTEffect extends Effect {
constructor() { super('CRT', frag, { uniforms: new Map([['uStrength', new Uniform(0)]]) }); }
}Wrap it for JSX with wrapEffect(CRTEffect) from @react-three/postprocessing, or render
<primitive object={crtEffect} /> inside the composer.
5. Put bloom first and make it cheap
Bloom should see the raw light, before colour splits and scanlines. resolutionScale={0.5} and
mipmapBlur halve its cost and look softer. Set multisampling={0} on the composer when you use
Pixelation or CRT, because anti-aliasing is wasted on a deliberately rough image.
6. Share one composer across Views
drei Views cut one canvas into regions. Put a single EffectComposer at the canvas level. A
composer per View means one full-screen pass per View, and the cost multiplies.
Pitfalls
- Noise not last. Grain under bloom glows. Put it at the end.
- Glitch on reduced motion. Set it to
DISABLEDand keep the CRT static.
Why it works
Shader compilation is the expensive part, not running the shader. Compile once, then every change is a number on a uniform, which is free.
See it live
[ex] in the wild · 14













