Skip to main content

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} />
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} />
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" />

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>
    </>
  );
}
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:
1

Open DevTools

Press F12 or Cmd+Option+I
2

Go to Performance tab

Enable “Screenshots” and “Web Vitals”
3

Record

Click record, interact with shaders, stop recording
4

Analyze

Look for long frames (>16ms for 60fps) and GPU activity

Memory Profiling

1

Open Memory tab in DevTools

Take heap snapshots before and after shader operations
2

Look for retained memory

Check if memory grows after disposing shaders
3

Find leaks

Compare snapshots to identify undisposed resources

Performance Checklist

  • Set speed={0} for static shaders
  • Use minPixelRatio={1} on mobile
  • Limit colors to 3-5 for mobile
  • Dispose shaders properly (vanilla only)
  • Optimize images to 2048×2048 or smaller
  • Reduce maxPixelCount on mobile
  • Pause shaders when out of viewport
  • Lazy load shader components
  • Use stable color references
  • Minimize grain parameters
  • Limit simultaneous shaders to 3-5
  • Test on actual mobile devices
  • Check FPS with DevTools
  • Monitor memory usage
  • Test on low-end devices
  • Verify battery impact

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

1

Reduce pixel ratio

Try minPixelRatio={1}
2

Lower max pixel count

Try maxPixelCount={1920 * 1080 * 2}
3

Simplify shader

Reduce colors, disable grain effects
4

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