I'm developing a full-screen rain animation using HTML, CSS, and JavaScript. I use a single <canvas> element with a requestAnimationFrame loop to animate 500 raindrops. Despite this, my GPU memory usage remains high and load times increase at high FPS. Below is a minimal code snippet that reproduces the issue:
const canvas = document.getElementById('rainCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const numDrops = 500;
const drops = [];
for (let i = 0; i < numDrops; i++) {
drops.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
speed: 2 + Math.random() * 4,
length: 10 + Math.random() * 10
});
}
function drawRain() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.strokeStyle = "rgba(255, 255, 255, 0.5)";
ctx.lineWidth = 2;
drops.forEach(drop => {
ctx.beginPath();
ctx.moveTo(drop.x, drop.y);
ctx.lineTo(drop.x, drop.y + drop.length);
ctx.stroke();
drop.y += drop.speed;
if (drop.y > canvas.height) {
drop.y = -drop.length;
}
});
requestAnimationFrame(drawRain);
}
drawRain();
body,
html {
margin: 0;
padding: 0;
overflow: hidden;
background-color: black;
}
canvas {
display: block;
}
<canvas id="rainCanvas"></canvas>
How can I optimize this canvas-based rain animation to reduce GPU load while maintaining smooth performance?
The only space for improvement is in the render loop.
The general rule of thumb is to avoid GPU state changes. Calls to ctx.stroke
and ctx.fill
force GPU state changes.
This means that data (style and path to stroke) is moved from the CPU to the GPU.
As all the strokes in your code are the same style you can render the whole scene in one call to ctx.stroke
.
Thus the change around the inner loop would be as follows
ctx.beginPath(); // ADDED. Do only once per loop
drops.forEach(drop => {
// REMOVED ctx.beginPath();
ctx.moveTo(drop.x, drop.y);
ctx.lineTo(drop.x, drop.y + drop.length);
// REMOVED ctx.stroke();
drop.y += drop.speed;
if (drop.y > canvas.height) { drop.y = -drop.length }
});
ctx.stroke(); // ADDED. Do once for all rain drops
Depending on the device, setup, GPU and number of drops this can provide a significant performance boost.
Note that if each stroke needed a different color/style you can group strokes with the same style and render each group with only one draw call (stroke and or fill)