Optimizing Konva Performance with rAF

Introduction

requestAnimationFrame (rAF) is a powerful browser API that helps achieve smoother interface changes by synchronizing rendering with the screen's refresh rate. Using rAF helps optimize performance, minimize lag (jank), and save energy by pausing calculations when the browser tab is inactive.

Prerequisites

In this article, I will continue to use the example from the previous article to optimize, so I will not provide the complete code but only the updated part; please refer to the previous article for the full content.


Detail

Update the OptimizedCanvas.tsx file as follows:

import Konva from 'konva'
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {Layer, Stage} from 'react-konva'
import useImage from 'use-image'
import {ShapeItem} from './ShapeItem'
import type {ShapeData, ViewRect} from './type'
import {generateData, HEIGHT, IMAGE_URL, TOTAL_OBJECTS, WIDTH} from './utility'

const OptimizedCanvas: React.FC = () => {
const [data] = useState<ShapeData[]>(generateData)
const [img] = useImage(IMAGE_URL)

const stageRef = useRef<Konva.Stage>(null)
const requestRef = useRef<number>(null)

const [viewRect, setViewRect] = useState<ViewRect>({
x1: -WIDTH,
y1: -HEIGHT,
x2: WIDTH,
y2: HEIGHT,
})

const updateViewport = useCallback(() => {
const stage = stageRef.current
if (!stage) return

if (requestRef.current) {
cancelAnimationFrame(requestRef.current)
}

requestRef.current = requestAnimationFrame(() => {
const scale = stage.scaleX()
const x = stage.x()
const y = stage.y()

setViewRect({
x1: -x / scale,
y1: -y / scale,
x2: (-x + WIDTH) / scale,
y2: (-y + HEIGHT) / scale,
})
})
}, [])

useEffect(() => {
updateViewport()
return () => {
if (requestRef.current) cancelAnimationFrame(requestRef.current)
}
}, [updateViewport])

const visibleItems = useMemo(() => {
const padding = 150
return data.filter(
item =>
item.x + padding > viewRect.x1 &&
item.x - padding < viewRect.x2 &&
item.y + padding > viewRect.y1 &&
item.y - padding < viewRect.y2
)
}, [data, viewRect])

const handleObjectClick = useCallback((id: string) => {
console.log('Clicked on object ID:', id)
}, [])

const handleWheel = useCallback(
(e: Konva.KonvaEventObject<WheelEvent>) => {
e.evt.preventDefault()
const stage = stageRef.current
if (!stage) return

const oldScale = stage.scaleX()
const pointer = stage.getPointerPosition()
if (!pointer) return

const scaleBy = 1.1
const newScale =
e.evt.deltaY < 0 ? oldScale * scaleBy : oldScale / scaleBy

stage.scale({x: newScale, y: newScale})

const newPos = {
x: pointer.x - (pointer.x - stage.x()) * (newScale / oldScale),
y: pointer.y - (pointer.y - stage.y()) * (newScale / oldScale),
}
stage.position(newPos)

updateViewport()
},
[updateViewport]
)

return (
<div className="relative w-full h-screen bg-[#121212] overflow-hidden select-none">
<Stage
width={WIDTH}
height={HEIGHT}
draggable
ref={stageRef}
onDragMove={updateViewport}
onWheel={handleWheel}
className="cursor-grab active:cursor-grabbing"
>
<Layer>
{visibleItems.map(item => (
<ShapeItem
key={item.id}
item={item}
image={img}
onClick={handleObjectClick}
/>
))}
</Layer>
</Stage>

<div className="absolute top-4 left-4 p-4 bg-black/70 backdrop-blur-md rounded-xl border border-white/10 pointer-events-none">
<h3 className="text-blue-400 font-bold text-xs uppercase mb-2">
Performance Monitor
</h3>
<div className="space-y-1 text-sm text-gray-300">
<div className="flex justify-between gap-8">
<span>Virtual Objects:</span>
<span className="font-mono text-white font-bold">
{TOTAL_OBJECTS.toLocaleString()}
</span>
</div>
<div className="flex justify-between gap-8">
<span>Active Rendered:</span>
<span className="font-mono text-green-400 font-bold">
{visibleItems.length}
</span>
</div>
<div className="flex justify-between gap-8">
<span>Current Zoom:</span>
<span className="font-mono text-yellow-400 font-bold">
{stageRef.current
? Math.round(stageRef.current.scaleX() * 100)
: 100}
%
</span>
</div>
</div>
<div className="mt-3 pt-3 border-t border-white/5 text-[10px] text-gray-500 italic">
Use Scroll to Zoom • Drag to Explore Space
</div>
</div>
</div>
)
}

export default OptimizedCanvas

  • The above code implements a Throttling mechanism via requestAnimationFrame to optimize updating the viewRect state when the user performs drag or zoom operations. By using requestRef to manage and cancel pending frames, the application ensures coordinate calculation occurs only once per screen refresh, combined with the Culling mechanism (rendering only visible elements) to significantly reduce CPU/GPU load when processing large numbers of objects.
    • Using requestAnimationFrame with a function passed inside means requesting that this function be executed before the next frame is drawn.
    • Note that cancelAnimationFrame only cancels the previously scheduled function execution, not the frame drawing itself.
  • Using requestAnimationFrame (rAF) to wrap the setViewRect command is a Debouncing/Throttling technique based on the screen's refresh rate. When you scroll or drag extremely fast, the onWheel and onDragMove events can fire hundreds of times per second, exceeding the smooth processing capability of React State and the screen's refresh rate.
  • Most screens today have a frequency of 60Hz (16.7ms/frame). If you scroll extremely fast, the browser may send 100 wheel events/second.
    • Without rAF: React attempts to re-render 100 times. This causes CPU churning, lag, and dropped frames.
    • With rAF: React updates at most 60 times/second, perfectly aligned with when the browser prepares to draw on the screen.


You can enable the Frame Rendering Stats function to see that the FPS level and GPU consumption have been optimized stably.

Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Using Kafka with Docker and NodeJS

Practicing with Google Cloud Platform - Google Kubernetes Engine to deploy nginx

Monitoring with cAdvisor, Prometheus and Grafana on Docker

Kubernetes Practice Series

NodeJS Practice Series

Sitemap