Documentation Index
Fetch the complete documentation index at: https://mintlify.com/shopify/react-native-skia/llms.txt
Use this file to discover all available pages before exploring further.
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
- 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>
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`);
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
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]);