Overview
Paper Shaders are designed for maximum performance, but understanding the optimization options helps you deliver smooth experiences across all devices.
Automatic Optimizations
Paper Shaders includes several automatic performance optimizations:
Automatic Pausing
Shaders automatically pause when the browser tab is hidden:
// Happens automatically - no code needed
// - Pauses requestAnimationFrame when tab hidden
// - Resumes when tab becomes visible
// - Saves CPU/GPU resources in background
Smart Resolution Scaling
Shaders automatically adjust rendering resolution based on:
Device pixel ratio (retina displays)
Browser zoom level
Pinch zoom on mobile
Container size changes
Uniform Caching
Only changed uniforms trigger GPU updates:
// Only u_distortion updates - other uniforms stay cached
shaderMount . setUniforms ({ u_distortion: 0.5 });
Resolution Control
Min Pixel Ratio
Controls minimum rendering quality:
// Default: 2x for crisp rendering even on 1x displays
< MeshGradient minPixelRatio = { 2 } />
// Lower for better performance on low-end devices
< MeshGradient minPixelRatio = { 1 } />
// Higher for ultra-sharp rendering
< MeshGradient minPixelRatio = { 3 } />
// Set during initialization
const shaderMount = new ShaderMount (
container ,
fragmentShader ,
uniforms ,
undefined ,
1 , // speed
0 , // frame
2 // minPixelRatio (default: 2)
);
// Update dynamically
shaderMount . setMinPixelRatio ( 1 );
Impact:
minPixelRatio={1}: Matches CSS pixels, fastest
minPixelRatio={2}: 4x pixels, great quality/performance balance (default)
minPixelRatio={3}: 9x pixels, ultra-sharp but slower
On 1x displays, minPixelRatio={2} still renders at 2x for better antialiasing.
On 2x displays (retina), it renders at actual device resolution.
Max Pixel Count
Limits total rendered pixels regardless of display size:
// Default: 1920 × 1080 × 4 = 8,294,400 pixels
< MeshGradient maxPixelCount = { 1920 * 1080 * 4 } />
// Lower for better performance
< MeshGradient maxPixelCount = { 1920 * 1080 * 2 } />
// Higher for 4K+ displays
< MeshGradient maxPixelCount = { 3840 * 2160 * 4 } />
// Set during initialization
const shaderMount = new ShaderMount (
container ,
fragmentShader ,
uniforms ,
undefined ,
1 , // speed
0 , // frame
2 , // minPixelRatio
1920 * 1080 * 4 // maxPixelCount
);
// Update dynamically
shaderMount . setMaxPixelCount ( 1920 * 1080 * 2 );
When to adjust:
Lower for mobile-first sites
Lower for complex shaders with many colors
Higher for desktop applications with large displays
Higher for simpler shaders on powerful hardware
Responsive Resolution
Adjust resolution based on device:
import { useState , useEffect } from 'react' ;
function ResponsiveShader () {
const [ device , setDevice ] = useState ( 'desktop' );
useEffect (() => {
const updateDevice = () => {
const width = window . innerWidth ;
if ( width < 768 ) setDevice ( 'mobile' );
else if ( width < 1024 ) setDevice ( 'tablet' );
else setDevice ( 'desktop' );
};
updateDevice ();
window . addEventListener ( 'resize' , updateDevice );
return () => window . removeEventListener ( 'resize' , updateDevice );
}, []);
const config = {
mobile: { minPixelRatio: 1 , maxPixelCount: 1920 * 1080 },
tablet: { minPixelRatio: 1.5 , maxPixelCount: 1920 * 1080 * 2 },
desktop: { minPixelRatio: 2 , maxPixelCount: 1920 * 1080 * 4 },
}[ device ];
return < MeshGradient { ... config } colors = { [ '#ff0000' , '#00ff00' ] } /> ;
}
Animation Performance
Static Shaders
Static shaders (speed=0) have zero ongoing performance cost:
// No requestAnimationFrame, no CPU/GPU usage after initial render
< MeshGradient speed = { 0 } colors = { [ '#ff0000' , '#00ff00' ] } />
Best for:
Background textures
Hero sections that don’t need motion
Mobile devices
Battery-conscious applications
Reduced Animation Speed
Slower animations use less CPU:
// Half speed = half the frame rate needed for smooth motion
< MeshGradient speed = { 0.5 } colors = { [ '#ff0000' , '#00ff00' ] } />
Conditional Animation
Animate only when visible:
import { useState , useEffect , useRef } from 'react' ;
function AnimateOnView () {
const [ isVisible , setIsVisible ] = useState ( false );
const ref = useRef ( null );
useEffect (() => {
const observer = new IntersectionObserver (
([ entry ]) => setIsVisible ( entry . isIntersecting ),
{ threshold: 0.1 }
);
if ( ref . current ) observer . observe ( ref . current );
return () => observer . disconnect ();
}, []);
return (
< div ref = { ref } >
< MeshGradient
speed = { isVisible ? 1 : 0 }
colors = { [ '#ff0000' , '#00ff00' ] }
/>
</ div >
);
}
The browser’s Page Visibility API automatically pauses shaders in background tabs.
This Intersection Observer pattern further optimizes for scrolled-out-of-view shaders.
Shader Complexity
Color Count
Fewer colors = better performance:
// Fastest: 2-3 colors
< MeshGradient colors = { [ '#ff0000' , '#00ff00' , '#0000ff' ] } />
// Good: 4-5 colors
< MeshGradient colors = { [ '#ff0000' , '#00ff00' , '#0000ff' , '#ffff00' , '#ff00ff' ] } />
// Slower: Maximum colors (10 for MeshGradient)
< MeshGradient colors = { [
'#ff0000' , '#00ff00' , '#0000ff' , '#ffff00' , '#ff00ff' ,
'#00ffff' , '#ff8800' , '#8800ff' , '#00ff88' , '#ff0088'
] } />
Performance impact:
Each color adds GPU calculations
2-4 colors: minimal impact
5-7 colors: noticeable on low-end devices
8+ colors: avoid on mobile
Parameter Complexity
Some parameters are more expensive:
// Cheaper parameters
< MeshGradient
distortion = { 0.8 } // Moderate cost
swirl = { 0.1 } // Moderate cost
/>
// Expensive parameters
< MeshGradient
grainMixer = { 0.5 } // Higher cost (adds noise calculations)
grainOverlay = { 0.5 } // Higher cost (post-processing)
/>
Optimization strategy:
Start with low/zero grain parameters
Increase only if needed visually
Test on target devices
Image-Based Shaders
Image Size
Larger images require more memory and processing:
// Optimize images before using
< Water
image = "/optimized-image.jpg" // Max 2048×2048 recommended
highlights = { 0.3 }
speed = { 1 }
/>
Best practices:
Resize images to actual display size
Use WebP or AVIF for better compression
Aim for 1024×1024 or smaller for filters
Use 2048×2048 max for high-quality needs
Mipmaps
Enable mipmaps for better performance with large images:
// React package automatically uses mipmaps for image shaders
< Water image = "/large-image.jpg" />
const shaderMount = new ShaderMount (
container ,
waterFragmentShader ,
uniforms ,
undefined ,
1 , // speed
0 , // frame
2 , // minPixelRatio
1920 * 1080 * 4 , // maxPixelCount
[ 'u_image' ] // Enable mipmaps for u_image
);
Image Loading
Preload images to avoid stutters:
import { useState , useEffect } from 'react' ;
function PreloadedWater () {
const [ imageLoaded , setImageLoaded ] = useState ( false );
useEffect (() => {
const img = new Image ();
img . onload = () => setImageLoaded ( true );
img . src = '/path/to/image.jpg' ;
}, []);
if ( ! imageLoaded ) {
return < div > Loading... </ div > ;
}
return < Water image = "/path/to/image.jpg" speed = { 1 } /> ;
}
Multiple Shaders
Lazy Loading
Load shaders on demand:
import { lazy , Suspense } from 'react' ;
const MeshGradient = lazy (() =>
import ( '@paper-design/shaders-react' ). then ( m => ({ default: m . MeshGradient }))
);
function App () {
return (
< Suspense fallback = { < div > Loading shader... </ div > } >
< MeshGradient colors = { [ '#ff0000' , '#00ff00' ] } />
</ Suspense >
);
}
Stagger Initialization
Initialize shaders one at a time:
import { useState , useEffect } from 'react' ;
function StaggeredShaders () {
const [ activeCount , setActiveCount ] = useState ( 1 );
useEffect (() => {
if ( activeCount < 3 ) {
const timer = setTimeout (() => setActiveCount ( prev => prev + 1 ), 100 );
return () => clearTimeout ( timer );
}
}, [ activeCount ]);
return (
<>
< MeshGradient colors = { [ '#ff0000' , '#00ff00' ] } />
{ activeCount >= 2 && < DotOrbit colors = { [ '#0000ff' , '#ffff00' ] } /> }
{ activeCount >= 3 && < SmokeRing colors = { [ '#ff00ff' , '#00ffff' ] } /> }
</>
);
}
Limit Active Shaders
Only render visible shaders:
function ShaderCarousel () {
const [ activeIndex , setActiveIndex ] = useState ( 0 );
const shaders = [
< MeshGradient key = "mesh" colors = { [ '#ff0000' , '#00ff00' ] } /> ,
< DotOrbit key = "dots" colors = { [ '#0000ff' , '#ffff00' ] } /> ,
< SmokeRing key = "smoke" colors = { [ '#ff00ff' , '#00ffff' ] } /> ,
];
return (
< div >
{ shaders [ activeIndex ] }
< button onClick = { () => setActiveIndex (( activeIndex + 1 ) % 3 ) } >
Next
</ button >
</ div >
);
}
Memory Management
Proper Cleanup
Always dispose shaders when unmounting:
// React components handle cleanup automatically
// No manual cleanup needed
function App () {
const [ show , setShow ] = useState ( true );
return (
<>
{ show && < MeshGradient colors = { [ '#ff0000' , '#00ff00' ] } /> }
< button onClick = { () => setShow ( ! show ) } > Toggle </ button >
</>
);
}
// Always call dispose() when removing shaders
const shaderMount = new ShaderMount ( /* ... */ );
// Later, when removing:
shaderMount . dispose ();
// Or on page unload:
window . addEventListener ( 'beforeunload' , () => {
shaderMount . dispose ();
});
Failing to dispose vanilla shaders causes memory leaks. Always call dispose()
when removing shaders from the page.
Stable References
Avoid creating new arrays on every render:
import { useMemo } from 'react' ;
// Bad: New array every render
function Bad () {
return < MeshGradient colors = { [ '#ff0000' , '#00ff00' ] } /> ;
}
// Good: Stable reference
const COLORS = [ '#ff0000' , '#00ff00' ];
function Good () {
return < MeshGradient colors = { COLORS } /> ;
}
// Also good: useMemo
function AlsoGood () {
const colors = useMemo (() => [ '#ff0000' , '#00ff00' ], []);
return < MeshGradient colors = { colors } /> ;
}
WebGL Context
Context Limits
Browsers limit concurrent WebGL contexts (typically 8-16):
// Check available contexts
function checkWebGLAvailability () {
const canvas = document . createElement ( 'canvas' );
const gl = canvas . getContext ( 'webgl2' );
if ( ! gl ) {
console . warn ( 'WebGL 2 not available' );
return false ;
}
return true ;
}
Best practices:
Limit to 3-5 simultaneous shaders per page
Dispose unused shaders to free contexts
Show fallback content if contexts exhausted
Context Attributes
Optimize context creation:
const contextAttributes = {
alpha: true , // Transparent backgrounds
antialias: false , // Disable for better performance
depth: false , // Not needed for 2D shaders
stencil: false , // Not needed for 2D shaders
preserveDrawingBuffer: false , // Better performance
powerPreference: 'default' , // Or 'low-power' for mobile
};
< MeshGradient
webGlContextAttributes = { contextAttributes }
colors = { [ '#ff0000' , '#00ff00' ] }
/>
Monitoring Performance
FPS Monitoring
let lastTime = performance . now ();
let frames = 0 ;
function measureFPS () {
frames ++ ;
const now = performance . now ();
if ( now >= lastTime + 1000 ) {
const fps = Math . round (( frames * 1000 ) / ( now - lastTime ));
console . log ( `FPS: ${ fps } ` );
frames = 0 ;
lastTime = now ;
}
requestAnimationFrame ( measureFPS );
}
measureFPS ();
Chrome DevTools
Use Chrome’s Performance panel:
Open DevTools
Press F12 or Cmd+Option+I
Go to Performance tab
Enable “Screenshots” and “Web Vitals”
Record
Click record, interact with shaders, stop recording
Analyze
Look for long frames (>16ms for 60fps) and GPU activity
Memory Profiling
Open Memory tab in DevTools
Take heap snapshots before and after shader operations
Look for retained memory
Check if memory grows after disposing shaders
Find leaks
Compare snapshots to identify undisposed resources
Performance Checklist
Performance Targets
Desktop
60 FPS with 2-3 shaders
2x pixel ratio
Full color complexity
Smooth animations
Mobile
30-60 FPS with 1-2 shaders
1-1.5x pixel ratio
Reduced color count
Consider static shaders
Low-End Devices
30 FPS with 1 shader
1x pixel ratio
Minimal colors (2-3)
Static shaders preferred
Troubleshooting
Low FPS
Reduce pixel ratio
Try minPixelRatio={1}
Lower max pixel count
Try maxPixelCount={1920 * 1080 * 2}
Simplify shader
Reduce colors, disable grain effects
Check other shaders
Limit to 1-2 simultaneous shaders
High Memory Usage
Dispose unused shaders
Optimize image sizes
Reduce number of simultaneous shaders
Check for memory leaks with DevTools
Stuttering
Preload images
Use stable prop references
Avoid creating new objects in render
Check browser’s Performance tab for long frames
Next Steps
Sizing & Fit Optimize shader sizing for performance
Customization Balance visual quality with performance
React Usage React-specific performance patterns
Vanilla Usage Vanilla JS performance control