React Native Skia supports two rendering paradigms: Retained Mode and Immediate Mode. Understanding when to use each is key to building performant graphics applications.
The Retained Mode allows for extremely fast animation with virtually zero FFI cost if the drawing list is updated at low frequency. Immediate mode allows for dynamic drawing lists but has a higher FFI cost.
Since both modes use the same <Canvas> element, you can seamlessly combine them in a single scene.
Retained Mode (Default)
In retained mode, you declare your scene as a tree of React components. React Native Skia converts this tree into a display list that is extremely efficient to animate with Reanimated.
This approach is extremely fast and best suited for user interfaces and interactive graphics where the structure doesn’t change at animation time.
import React, { useEffect } from "react";
import { Canvas, Circle, Group } from "@shopify/react-native-skia";
import {
useSharedValue,
withSpring
} from "react-native-reanimated";
export const RetainedModeExample = () => {
const radius = useSharedValue(50);
useEffect(() => {
radius.value = withSpring(radius.value === 50 ? 100 : 50);
}, []);
return (
<Canvas style={{ flex: 1 }}>
<Group>
<Circle cx={128} cy={128} r={radius} color="cyan" />
</Group>
</Canvas>
);
};
In immediate mode, you issue drawing commands directly to a canvas on every frame. This gives you complete control over what gets drawn and when, but requires you to manage the drawing logic yourself.
React Native Skia provides immediate mode through the Picture API. This mode is well-suited for scenes where the number of drawing commands changes on every animation frame, such as:
- Games
- Generative art
- Particle systems
- Dynamic visualizations
import { Canvas, Picture, Skia } from "@shopify/react-native-skia";
import {
useDerivedValue,
useSharedValue,
withRepeat,
withTiming
} from "react-native-reanimated";
import { useEffect } from "react";
const size = 256;
export const ImmediateModeExample = () => {
const progress = useSharedValue(0);
const recorder = Skia.PictureRecorder();
const paint = Skia.Paint();
useEffect(() => {
progress.value = withRepeat(
withTiming(1, { duration: 2000 }),
-1,
true
);
}, [progress]);
const picture = useDerivedValue(() => {
"worklet";
const canvas = recorder.beginRecording(
Skia.XYWHRect(0, 0, size, size)
);
// Variable number of circles based on progress
const count = Math.floor(progress.value * 20);
for (let i = 0; i < count; i++) {
const r = (i + 1) * 6;
paint.setColor(
Skia.Color(`rgba(0, 122, 255, ${(i + 1) / 20})`)
);
canvas.drawCircle(size / 2, size / 2, r, paint);
}
return recorder.finishRecordingAsPicture();
});
return (
<Canvas style={{ flex: 1 }}>
<Picture picture={picture} />
</Canvas>
);
};
Choosing the Right Mode
Here’s a guide to help you choose the appropriate rendering mode:
| Scenario | Recommended Mode | Why |
|---|
| UI with animated properties | Retained | Zero FFI cost during animation |
| Data visualization | Retained | Structure usually fixed |
| Fixed number of sprites/tiles | Retained | With Atlas API, single draw call |
| Game with dynamic entities | Immediate | Entities created/destroyed |
| Procedural/generative art | Immediate | Dynamic drawing commands |
| Particle systems | Immediate | Variable particle count |
| Static graphics | Retained | Simplest API |
Combining Modes
You can combine both rendering modes in a single scene. For example, a game where the scene is dynamic (immediate mode) but the UI elements are built in retained mode.
import { Canvas, Picture, Rect } from "@shopify/react-native-skia";
const HybridExample = () => {
return (
<Canvas style={{ flex: 1 }}>
{/* Immediate mode for dynamic content */}
<Picture picture={dynamicPicture} />
{/* Retained mode for UI elements */}
<Rect x={10} y={10} width={100} height={40} color="white" />
</Canvas>
);
};