Canvas Animation in React: Principles, Implementation, and Performance Optimization
This article explains the fundamentals of web animation, compares animation techniques such as CSS, SVG, and Canvas, demonstrates how to create frame‑based Canvas animations with JavaScript and Konva, shows how to integrate them into React (including react‑konva), and provides performance‑optimisation strategies for smoother rendering.
With mobile hardware becoming more powerful, web pages are adopting richer interactions and gestures, narrowing the gap with native apps. Besides CSS animations, developers often use Canvas or SVG for complex visual effects.
React remains popular, and this article shares a method for implementing Canvas animations in React along with performance optimisations.
1. Basic animation principle
Human eyes perceive a rapid sequence of images as continuous motion; the number of frames per second (FPS) measures smoothness, with 30 FPS generally considered fluid.
2. Web animation techniques
Beyond CSS, the main web animation carriers are SVG, Canvas, and WebGL, which enable more complex content such as game graphics. For fine‑grained motion like character walking or jumping, frame (or sprite‑sheet) animation is used, splitting a timeline into sequential keyframes.
Common ways to implement frame animation include:
GIF – low cost and easy to use, but limited to 256 colors and poor alpha support.
CSS – convenient and GPU‑accelerated via translate3d , but synchronising multiple animations can be complex.
JavaScript – draws on Canvas (or other carriers) with full control, supporting any image type at the cost of higher CPU usage.
The article focuses on the JavaScript approach and its migration to React.
3. Implementing animation with JavaScript
Typically a rendering framework is used to simplify drawing; otherwise native APIs can be used directly. Various frameworks (Lottie, PixiJS, Three.js, CreateJS, Konva) exist, and the article chooses Konva for its simplicity.
Basic Canvas animation can be summarised as a "timed redraw" loop:
function tick() {
// draw animation content
requestAnimationFrame(tick);
};
// start the loop
tick();Using Konva, a simple rectangle moving horizontally is created:
const stage = new Konva.Stage({
container: 'container',
width: 100,
height: 100,
});
const layer = new Konva.Layer();
stage.add(layer);
let x = 0;
const rect = new Konva.Rect({
x: x,
y: 20,
width: 50,
height: 50,
fill: 'red',
});
layer.add(rect);
function tick() {
x += 1;
rect.setAttr('x', x);
rect.draw();
if (x > 30) return;
requestAnimationFrame(tick);
};
tick();To avoid direct DOM manipulation, the same logic is moved into a React component, creating a div container for Konva:
function createPic(canvasContainer) {
const stage = new Konva.Stage({
container: canvasContainer,
width: 100,
height: 100,
});
return stage;
}
function DrawCanvas() {
const ref = useRef();
useEffect(() => {
const stage = createPic(ref.current);
return () => { stage.destroy(); };
}, []);
return
;
}Because react‑dom cannot render Konva objects directly, the article introduces react‑konva , which provides React components ( Stage , Layer , Rect , etc.) that map to Konva nodes via a custom renderer built on react-reconciler .
Key parts of the custom renderer include:
createInstance – creates a Konva node based on the element type.
appendInitialChild – adds a child node to its parent.
commitUpdate – applies updated props to a Konva instance.
isPrimaryRenderer = false – makes the renderer auxiliary to react‑dom.
Example of the host config implementation:
function createInstance(type, props, rootContainer, hostContext, internalHandle) {
let NodeClass = Konva[type];
const propsWithoutEvents = excludeEvts(props);
const instance = new NodeClass(propsWithoutEvents);
return instance;
}
function appendInitialChild(parentInstance, child) {
if (typeof child === 'string') {
console.error(`Do not use plain text as child of Konva.Node. You are using text: ${child}`);
return;
}
parentInstance.add(child);
}After setting up the custom renderer, the animation loop is added inside a functional component:
const Picture = () => {
const updateRef = useRef();
const xRef = useRef(0);
const [x, setX] = useState(0);
updateRef.current = setX;
useEffect(() => {
let id;
const tick = () => {
xRef.current += 1;
updateRef.current(xRef.current);
if (xRef.current > 30) return;
id = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(id);
}, []);
return (
);
};4. Optimization
The article notes that updating state on every frame can cause noticeable performance loss, as each state change triggers a full React render cycle. To mitigate this, it suggests moving mutable properties to refs and updating the Konva node directly, bypassing React's diffing:
rectRef.current.setAttr('x', xRef.current);
updatePicture(rectRef.current);After refactoring, the component looks like:
const Picture = () => {
const rectRef = useRef();
const xRef = useRef(0);
useEffect(() => {
let id;
const tick = () => {
xRef.current += 1;
rectRef.current.setAttr('x', xRef.current);
updatePicture(rectRef.current);
if (xRef.current > 30) return;
id = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(id);
}, []);
return (
);
};By reducing reliance on React state and props for per‑frame updates, the animation becomes smoother and consumes fewer resources.
Conclusion
React’s extensibility allows developers to build custom renderers such as react‑konva, enabling Canvas‑based animations to coexist with standard DOM rendering. While this approach simplifies development, careful handling of per‑frame updates—preferably via refs and direct Konva manipulation—remains essential for maintaining performance in animation‑heavy applications.
Ctrip Technology
Official Ctrip Technology account, sharing and discussing growth.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.