Skip to main content

Overview

React ThorVG Fiber offers two rendering modes: Software (CPU) and WebGL (GPU). Understanding their performance characteristics and optimization techniques is crucial for building high-performance graphics applications.

Choosing the Right Renderer

SwCanvas (Software Renderer)

Best for:
  • Simple graphics with few shapes (< 100 shapes)
  • Maximum compatibility across devices
  • Environments without WebGL support
  • Static or infrequently updated graphics
Characteristics:
  • CPU-based rendering
  • Linear performance degradation with shape count
  • No GPU overhead
  • Works in all browsers and environments
Performance Profile:
  • Excellent for < 50 shapes
  • Good for 50-100 shapes
  • Degrades for 100+ shapes
  • Poor for 500+ shapes

GlCanvas (WebGL Renderer)

Best for:
  • Complex graphics with many shapes (> 100 shapes)
  • Animations and frequent updates
  • Performance-critical applications
  • Modern browsers with WebGL support
Characteristics:
  • GPU-accelerated rendering
  • Better scaling with shape count
  • Initial WebGL context overhead
  • Requires WebGL support
Performance Profile:
  • Overhead for < 50 shapes (use SwCanvas instead)
  • Good for 50-500 shapes
  • Excellent for 500+ shapes
  • Can handle 1000+ shapes with good performance

Performance Comparison

Based on real-world testing with 1000 animated rectangles:
MetricSwCanvasGlCanvasWinner
Rendering FPS~15-20~60GlCanvas
Initial LoadFastSlowerSwCanvas
Memory UsageLowerHigherSwCanvas
Shape Limit~100~1000+GlCanvas
Compatibility100%~95%SwCanvas

Optimization Techniques

1. Use devicePixelRatio Wisely

The devicePixelRatio prop controls rendering resolution. Higher values look sharper but cost more performance.
<SwCanvas 
  width={500} 
  height={500} 
  devicePixelRatio={2} // Good balance
  locateFile={locateFile}
>
  {/* shapes */}
</SwCanvas>
Impact: Doubling DPR quadruples the number of pixels to render.

2. Minimize Shape Count

Reduce the number of shapes whenever possible:
// ❌ Bad - 4 separate shapes
<>
  <Shape fill={[255, 0, 0, 255]}>
    <Rect x={0} y={0} width={10} height={10} />
  </Shape>
  <Shape fill={[255, 0, 0, 255]}>
    <Rect x={20} y={0} width={10} height={10} />
  </Shape>
  <Shape fill={[255, 0, 0, 255]}>
    <Rect x={40} y={0} width={10} height={10} />
  </Shape>
  <Shape fill={[255, 0, 0, 255]}>
    <Rect x={60} y={0} width={10} height={10} />
  </Shape>
</>

// ✅ Good - 1 shape with complex path
<Shape fill={[255, 0, 0, 255]}>
  <Path>
    <MoveTo x={0} y={0} />
    <LineTo x={10} y={0} />
    <LineTo x={10} y={10} />
    <LineTo x={0} y={10} />
    <Close />
    <MoveTo x={20} y={0} />
    {/* ... combine paths */}
  </Path>
</Shape>

3. Memoize Static Content

Use React’s memoization to prevent unnecessary re-renders:
import { memo, useMemo } from "react";

const StaticShapes = memo(() => {
  return (
    <>
      <Shape x={100} y={100} fill={[255, 0, 0, 255]}>
        <Rect x={-50} y={-50} width={100} height={100} />
      </Shape>
      <Shape x={300} y={100} fill={[0, 0, 255, 255]}>
        <Circle cx={0} cy={0} rx={50} ry={50} />
      </Shape>
    </>
  );
});

function App() {
  const [rotation, setRotation] = useState(0);
  
  return (
    <SwCanvas locateFile={locateFile}>
      <StaticShapes /> {/* Won't re-render when rotation changes */}
      
      <Shape rotation={rotation} fill={[0, 255, 0, 255]}>
        <Rect x={-25} y={-25} width={50} height={50} />
      </Shape>
    </SwCanvas>
  );
}

4. Optimize Shape Data

Pre-calculate shape data outside of components:
// ✅ Good - calculated once
const shapeData = Array.from({ length: 100 }, (_, i) => ({
  x: (i % 10) * 50,
  y: Math.floor(i / 10) * 50,
  color: [Math.random() * 255, Math.random() * 255, Math.random() * 255, 255] as [number, number, number, number],
}));

function App() {
  return (
    <SwCanvas locateFile={locateFile}>
      {shapeData.map((shape, i) => (
        <Shape key={i} x={shape.x} y={shape.y} fill={shape.color}>
          <Rect x={-10} y={-10} width={20} height={20} />
        </Shape>
      ))}
    </SwCanvas>
  );
}
// ❌ Bad - recalculated every render
function App() {
  return (
    <SwCanvas locateFile={locateFile}>
      {Array.from({ length: 100 }, (_, i) => (
        <Shape 
          key={i} 
          x={(i % 10) * 50} 
          y={Math.floor(i / 10) * 50}
          fill={[Math.random() * 255, Math.random() * 255, Math.random() * 255, 255]}
        >
          <Rect x={-10} y={-10} width={20} height={20} />
        </Shape>
      ))}
    </SwCanvas>
  );
}

5. Use Stable Keys

Provide stable keys for lists of shapes:
// ✅ Good - stable keys
const shapes = [
  { id: 'rect-1', x: 100, y: 100 },
  { id: 'rect-2', x: 200, y: 100 },
];

<>
  {shapes.map(shape => (
    <Shape key={shape.id} x={shape.x} y={shape.y}>
      <Rect x={-25} y={-25} width={50} height={50} />
    </Shape>
  ))}
</>

// ❌ Bad - index keys (unstable when list changes)
<>
  {shapes.map((shape, i) => (
    <Shape key={i} x={shape.x} y={shape.y}>
      <Rect x={-25} y={-25} width={50} height={50} />
    </Shape>
  ))}
</>

6. Optimize Animations

For smooth animations, update state efficiently:
import { useEffect, useRef, useState } from "react";
import { gsap } from "gsap";

function AnimatedCanvas() {
  const [rotation, setRotation] = useState(0);
  const rotationRef = useRef({ value: 0 });

  useEffect(() => {
    // Use GSAP for smooth animations
    const tween = gsap.to(rotationRef.current, {
      value: 360,
      duration: 2,
      repeat: -1,
      ease: "none",
      onUpdate: () => {
        setRotation(rotationRef.current.value);
      },
    });

    return () => tween.kill();
  }, []);

  return (
    <GlCanvas id="canvas" locateFile={locateFile}>
      <Shape rotation={rotation} fill={[255, 0, 0, 255]}>
        <Rect x={-50} y={-50} width={100} height={100} />
      </Shape>
    </GlCanvas>
  );
}

7. Batch Updates

Group related state updates:
import { useCallback, useState } from "react";

function App() {
  const [state, setState] = useState({
    rotation: 0,
    x: 100,
    y: 100,
  });

  const updateShape = useCallback((updates: Partial<typeof state>) => {
    // ✅ Single state update
    setState(prev => ({ ...prev, ...updates }));
  }, []);

  return (
    <SwCanvas locateFile={locateFile}>
      <Shape 
        x={state.x} 
        y={state.y} 
        rotation={state.rotation}
        fill={[255, 0, 0, 255]}
      >
        <Rect x={-25} y={-25} width={50} height={50} />
      </Shape>
    </SwCanvas>
  );
}

8. Lazy Load Canvas

Load canvas components only when needed:
import { lazy, Suspense } from "react";

const ThorVGCanvas = lazy(() => import("./components/Canvas"));

function App() {
  return (
    <Suspense fallback={<div>Loading canvas...</div>}>
      <ThorVGCanvas />
    </Suspense>
  );
}

9. Optimize WASM Loading

Preload WASM files for faster initialization:
import { useEffect } from "react";
import wasmUrl from "react-thorvg-fiber/thorvg-sw.wasm?url";

function App() {
  useEffect(() => {
    // Preload WASM file
    const link = document.createElement('link');
    link.rel = 'preload';
    link.href = wasmUrl;
    link.as = 'fetch';
    link.type = 'application/wasm';
    link.crossOrigin = 'anonymous';
    document.head.appendChild(link);
  }, []);

  // ...
}

10. Monitor Performance

Use React DevTools Profiler and browser performance tools:
import { Profiler } from "react";

function App() {
  const onRender = (id: string, phase: string, actualDuration: number) => {
    console.log(`${id} (${phase}) took ${actualDuration}ms`);
  };

  return (
    <Profiler id="Canvas" onRender={onRender}>
      <SwCanvas locateFile={locateFile}>
        {/* shapes */}
      </SwCanvas>
    </Profiler>
  );
}

Real-World Example

Here’s an optimized example rendering 1000 animated shapes:
import { useCallback, useEffect, useRef, useState } from "react";
import { GlCanvas, Shape, Rect } from "react-thorvg-fiber";
import { gsap } from "gsap";
import wasmUrl from "react-thorvg-fiber/thorvg-gl.wasm?url";

// Pre-calculate static shape data
const SHAPE_COUNT = 1000;
const GRID_SIZE = Math.ceil(Math.sqrt(SHAPE_COUNT));
const CANVAS_SIZE = 500;
const SHAPE_SIZE = CANVAS_SIZE / GRID_SIZE;

const shapeData = Array.from({ length: SHAPE_COUNT }, (_, i) => {
  const row = Math.floor(i / GRID_SIZE);
  const col = i % GRID_SIZE;
  const hue = (i * 360) / SHAPE_COUNT;
  
  return {
    key: `shape-${i}`,
    x: col * SHAPE_SIZE + SHAPE_SIZE / 2,
    y: row * SHAPE_SIZE + SHAPE_SIZE / 2,
    color: hslToRgb(hue, 0.7, 0.6),
  };
});

function hslToRgb(h: number, s: number, l: number): [number, number, number, number] {
  // HSL to RGB conversion (implementation omitted for brevity)
  // Returns [r, g, b, 255]
}

function OptimizedCanvas() {
  const [rotation, setRotation] = useState(0);
  const rotationRef = useRef({ value: 0 });

  const locateFile = useCallback(() => wasmUrl, []);

  useEffect(() => {
    const tween = gsap.to(rotationRef.current, {
      value: 360,
      duration: 10,
      repeat: -1,
      ease: "none",
      onUpdate: () => {
        setRotation(rotationRef.current.value);
      },
    });

    return () => tween.kill();
  }, []);

  return (
    <GlCanvas 
      id="optimized-canvas"
      width={CANVAS_SIZE} 
      height={CANVAS_SIZE} 
      devicePixelRatio={2}
      locateFile={locateFile}
    >
      {shapeData.map(shape => (
        <Shape 
          key={shape.key}
          x={shape.x} 
          y={shape.y} 
          fill={shape.color}
          rotation={rotation}
        >
          <Rect 
            x={-SHAPE_SIZE / 2} 
            y={-SHAPE_SIZE / 2} 
            width={SHAPE_SIZE} 
            height={SHAPE_SIZE} 
          />
        </Shape>
      ))}
    </GlCanvas>
  );
}

export default OptimizedCanvas;

Performance Checklist

  • Choose the right renderer (SwCanvas vs GlCanvas) based on shape count
  • Set appropriate devicePixelRatio (default: 2)
  • Minimize total number of shapes
  • Pre-calculate static shape data outside components
  • Use memo() for static shape groups
  • Provide stable keys for shape lists
  • Wrap locateFile in useCallback()
  • Batch related state updates
  • Use efficient animation libraries (GSAP, Framer Motion)
  • Preload WASM files for faster initialization
  • Profile with React DevTools and browser performance tools

Benchmarking Results

Based on testing with different shape counts: SwCanvas Performance:
  • 10 shapes: 60 FPS
  • 50 shapes: 60 FPS
  • 100 shapes: 45-60 FPS
  • 500 shapes: 15-20 FPS
  • 1000 shapes: 10-15 FPS
GlCanvas Performance:
  • 10 shapes: 60 FPS (overkill)
  • 50 shapes: 60 FPS
  • 100 shapes: 60 FPS
  • 500 shapes: 60 FPS
  • 1000 shapes: 55-60 FPS
Recommendation: Use SwCanvas for ≤ 100 shapes, GlCanvas for > 100 shapes.

Next Steps

Build docs developers (and LLMs) love