Skip to main content
React Native Skia is built for high performance, but following best practices ensures your animations and graphics run smoothly at 60fps or higher.

General Principles

Minimize Re-renders

Avoid unnecessary canvas re-renders by using proper state management:
// ❌ Bad - recreates object on every render
<Circle cx={100} cy={100} r={50} color={{ r: 1, g: 0, b: 0 }} />

// ✅ Good - stable reference
const redColor = { r: 1, g: 0, b: 0 };
<Circle cx={100} cy={100} r={50} color={redColor} />

// ✅ Best - use string colors
<Circle cx={100} cy={100} r={50} color="red" />

Use Worklets for Animations

Reanimated worklets run on the UI thread for smooth animations:
import { useSharedValue } from "react-native-reanimated";
import { Canvas, Circle } from "@shopify/react-native-skia";

export default function SmoothAnimation() {
  const cx = useSharedValue(100);
  
  // Animation runs on UI thread
  useEffect(() => {
    cx.value = withRepeat(withTiming(200), -1, true);
  }, []);
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Circle cx={cx} cy={150} r={50} color="blue" />
    </Canvas>
  );
}

Path Optimization

Mark Paths as Volatile

For paths that change frequently:
const path = Skia.Path.Make();
path.moveTo(0, 0);
path.lineTo(100, 100);
path.setIsVolatile(true); // Prevents caching for frequently changing paths

Simplify Complex Paths

Use path simplification for better performance:
const complexPath = createComplexPath();
complexPath.simplify(); // Reduces complexity while maintaining visual appearance

Reuse Path Objects

// ❌ Bad - creates new path every frame
const AnimatedPath = () => {
  const progress = useSharedValue(0);
  
  return (
    <Path
      path={() => {
        const path = Skia.Path.Make(); // New allocation!
        // ...
        return path;
      }}
    />
  );
};

// ✅ Good - reuse path object
const AnimatedPath = () => {
  const progress = useSharedValue(0);
  const path = useMemo(() => Skia.Path.Make(), []);
  
  return (
    <Path
      path={() => {
        path.rewind(); // Reset path
        // ... build path
        return path;
      }}
    />
  );
};

Image Optimization

Use Appropriate Image Formats

  • PNG for images with transparency
  • JPEG for photos without transparency
  • WebP for best compression

Preload Images

import { useImage } from "@shopify/react-native-skia";

export default function OptimizedImages() {
  // Load image once, reuse across renders
  const image = useImage(require("./large-image.jpg"));
  
  if (!image) {
    return <LoadingView />;
  }
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Image image={image} x={0} y={0} width={300} height={300} fit="cover" />
    </Canvas>
  );
}

Use Image Sampling Options

Control image quality vs performance:
// Low quality, fast rendering
<Image
  image={image}
  fit="cover"
  sampling={{ filter: FilterMode.Nearest }}
/>

// High quality, slower rendering  
<Image
  image={image}
  fit="cover"
  sampling={{ B: 1/3, C: 1/3 }} // Mitchell-Netravali cubic
/>

Layer and Effect Optimization

Minimize Layers

Layers create offscreen buffers - use them sparingly:
// ❌ Bad - unnecessary layer
<Group layer>
  <Circle cx={100} cy={100} r={50} color="red" />
</Group>

// ✅ Good - only use layers when needed for effects
<Group layer={<Paint><Blur blur={10} /></Paint>}>
  <Circle cx={100} cy={100} r={50} color="red" />
</Group>

Cache Complex Effects

Use useMemo for expensive shader or filter creation:
const blurFilter = useMemo(
  () => Skia.ImageFilter.MakeBlur(10, 10, TileMode.Decal, null),
  []
);

<Group layer={<Paint imageFilter={blurFilter} />}>
  {/* content */}
</Group>

Font and Text Optimization

Preload Fonts

import { matchFont, useFonts } from "@shopify/react-native-skia";

export default function TextExample() {
  const fontsLoaded = useFonts({
    "Roboto-Regular": [require("./Roboto-Regular.ttf")],
    "Roboto-Bold": [require("./Roboto-Bold.ttf")],
  });
  
  const font = matchFont({ fontFamily: "Roboto", fontSize: 16 });
  
  if (!fontsLoaded) return null;
  
  return (
    <Canvas style={{ flex: 1 }}>
      <Text x={50} y={100} text="Hello" font={font} />
    </Canvas>
  );
}

Use TextBlob for Static Text

For text that doesn’t change:
const textBlob = useMemo(
  () => Skia.TextBlob.MakeFromText("Static Text", font),
  [font]
);

<TextBlob blob={textBlob} x={50} y={100} />

Rendering Optimization

Use mode="continuous" Wisely

Only use continuous rendering when necessary:
// ❌ Bad - always re-rendering
<Canvas mode="continuous" style={{ flex: 1 }}>
  <Circle cx={100} cy={100} r={50} color="red" />
</Canvas>

// ✅ Good - default mode, only re-renders when needed
<Canvas style={{ flex: 1 }}>
  <Circle cx={100} cy={100} r={50} color="red" />
</Canvas>

Batch Updates

Group related drawing operations:
// ✅ Good - all circles rendered in one pass
<Group>
  {points.map((point, i) => (
    <Circle key={i} cx={point.x} cy={point.y} r={5} color="blue" />
  ))}
</Group>

Memory Management

Dispose of Resources

Manually dispose of large objects when done:
useEffect(() => {
  const surface = Skia.Surface.Make(1000, 1000);
  // Use surface...
  
  return () => {
    // Cleanup happens automatically for JSI objects
    // but you can help by removing references
  };
}, []);

Avoid Memory Leaks in Animations

useEffect(() => {
  const animation = requestAnimationFrame(animate);
  return () => cancelAnimationFrame(animation);
}, []);

Profiling and Debugging

Enable Debug Mode

<Canvas debug style={{ flex: 1 }}>
  {/* Shows render time in development */}
</Canvas>

Use React DevTools Profiler

Identify unnecessary re-renders in your component tree.

Measure Frame Times

const frameStart = performance.now();
// ... rendering code
const frameTime = performance.now() - frameStart;
console.log(`Frame time: ${frameTime}ms`);

Platform-Specific Optimizations

Android

  • Enable hardware acceleration in AndroidManifest.xml
  • Use androidWarmup prop to pre-initialize Skia
<Canvas androidWarmup style={{ flex: 1 }}>
  {/* content */}
</Canvas>

iOS

  • Leverage Metal backend (enabled by default)
  • Use P3 color space for wider color gamut:
<Canvas colorSpace="p3" style={{ flex: 1 }}>
  {/* content */}
</Canvas>

Best Practices Checklist

  • ✅ Use Reanimated worklets for animations
  • ✅ Minimize layer usage
  • ✅ Preload images and fonts
  • ✅ Reuse path objects
  • ✅ Mark frequently changing paths as volatile
  • ✅ Use useMemo for expensive operations
  • ✅ Avoid creating new objects in render methods
  • ✅ Profile your app regularly
  • ✅ Simplify complex paths
  • ✅ Use appropriate image formats and sampling
  • ✅ Batch drawing operations
  • ✅ Clean up resources in useEffect cleanup

Common Performance Pitfalls

Creating Objects in Render

// ❌ Bad
<LinearGradient
  start={{ x: 0, y: 0 }}  // New object every render
  end={{ x: 100, y: 100 }}
  colors={["red", "blue"]}
/>

// ✅ Good
const start = useMemo(() => ({ x: 0, y: 0 }), []);
const end = useMemo(() => ({ x: 100, y: 100 }), []);
<LinearGradient start={start} end={end} colors={["red", "blue"]} />

Excessive Nesting

// ❌ Bad - deeply nested groups
<Group>
  <Group>
    <Group>
      <Circle />
    </Group>
  </Group>
</Group>

// ✅ Good - flat structure
<Group>
  <Circle />
</Group>

Not Using Memoization

// ❌ Bad - recalculates every render
const complexPath = () => {
  const path = Skia.Path.Make();
  // ... expensive calculations
  return path;
};

// ✅ Good - memoized
const complexPath = useMemo(() => {
  const path = Skia.Path.Make();
  // ... expensive calculations
  return path;
}, [dependencies]);

Build docs developers (and LLMs) love