optimizing performance in a JavaScript animation: starfall with canvas and pixels?

I have a JavaScript animation that simulates a starfall effect using HTML canvas and “pixel” manipulation. While the animation works, I’m facing efficiency and speed issues, and I’m looking for ways to optimize the code. The animation involves creating stars that fall from the top of the screen and leaving a trail of pixels as they move. The thing is, the pixels are divs, and it reaches thousands.

the current JS code:

const content = document.getElementById("bkg-content");
const background = document.getElementById("bkg");
const ctx = content.getContext("2d", { willReadFrequently: true });

let stars = [];
const maxStars = 20; // Maximum number of stars

let last_x = 0;
let next_spawn = 0;

function getX(size) {
    let random = Math.random() * (window.innerWidth - size);
    return last_x - size > random || random > last_x + size ? random : getX(size);
}

function createStar() {
    let transparency = Math.random();
    let size = transparency * 200 + 100;
    let x = getX(size)
    let fallingSpeed = Math.random() * 30 + 20;
    next_spawn = size / fallingSpeed * 500;

    last_x = x;
    return {
        x,
        y: 0,
        size,
        transparency,
        rotationAngle: 0,
        rotationSpeed: Math.random() * 0.3 + 0.05,
        rotationDirection: Math.random() < 0.5 ? 1 : -1,
        fallingSpeed,
    };
}

function spawnStars(count) {
    for (let i = 0; i < count; i++) {
        stars.push(createStar());
    }

    setTimeout(() => {
        spawnStars(1)
    }, next_spawn)

    console.log(stars.length);
}

function draw() {
    ctx.clearRect(0, 0, content.width, content.height);

    for (let i = 0; i < stars.length; i++) {
        let star = stars[i];
        star.rotationAngle += star.rotationSpeed * star.rotationDirection;
        star.y += star.fallingSpeed;

        if (star.y > window.innerHeight + star.size / 2) {
            // Remove stars that have completely passed the bottom of the screen
            stars.splice(i, 1);
            i--; // Adjust the loop variable since the array length has changed
        } else {
            ctx.save();
            ctx.translate(star.x + star.size / 2, star.y);
            ctx.rotate(star.rotationAngle);

            // Adjust transparency based on star size
            ctx.globalAlpha = star.transparency;
            console.log(ctx.globalAlpha)

            ctx.drawImage(starTXT, -star.size / 2, -star.size / 2, star.size, star.size);

            // Reset global alpha for subsequent drawings
            ctx.globalAlpha = 1;
            ctx.restore();
        }
    }
}


function resizeCanvas() {
    content.width = window.innerWidth;
    content.height = window.innerHeight;

    spawnStars(Math.min(maxStars, 1)); // Initially spawn one star

    setInterval(() => {
        createPixels()
        draw()
    }, 500);
}

function createPixels() {
    const pixel = { size: 9, gap: 3 };
    const numX = Math.floor(window.innerWidth / (pixel.size + pixel.gap));
    const numY = Math.floor(window.innerHeight / (pixel.size + pixel.gap));

    background.style.gridTemplateColumns = `repeat(${numX}, ${pixel.size}px)`;
    background.innerHTML = ''; // clear existing pixels

    for (let x = 0; x < numX; x++) {
        for (let y = 0; y < numY; y++) {
            const child = document.createElement("div");
            child.classList.add("pixel");
            background.appendChild(child);
        }
    }

    const children = Array.from(background.getElementsByClassName("pixel"));

    children.forEach((child) => {
        const rect = child.getBoundingClientRect();
        const { data } = ctx.getImageData(rect.left, rect.top, pixel.size, pixel.size);

        const averageColor = getAverageColor(data);
        const color = `rgb(${averageColor}, ${averageColor}, ${averageColor})`;

        child.style.backgroundColor = color;
        child.style.boxShadow = `0 0 10px ${color}`;
    });
}

function getAverageColor(data) {
    let sum = 0;

    for (let i = 0; i < data.length; i += 4) {
        sum += data[i];
    }


    const averageColor = sum / (data.length / 4);

    return averageColor;
}

const starTXT = new Image(1600, 1600);
starTXT.src = "star.png";

starTXT.onload = resizeCanvas;

window.addEventListener('resize', resizeCanvas);

Another problem is resizing, so that it is optimal and fast, the code lags lots. Any tips? Any different approaches I could take?

the result I aimed for

The current implementation seems to have performance bottlenecks. I suspect the loop in the draw function and the frequent manipulation of the canvas might be causing slowdowns. How can I optimize the code to improve efficiency? The animation speed is crucial for a smooth user experience. Are there any specific techniques or best practices I should follow to enhance the speed of the starfall effect? I’m using a combination of canvas drawing for stars and pixel manipulation for the background. Is this the most optimal approach, or are there alternative methods that could provide better performance?

I’ve already tried some basic optimizations like using requestAnimationFrame instead of setInterval, but I’m looking for more insights and suggestions from the community. Any help in identifying and addressing the performance issues in the code would be greatly appreciated.

Sorry if my code is any messy or unreadable, I’ve tried a lot.

EDIT: the HTML file is underneath

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>...</title>

    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            height: 100vh;
            background-color: black;
            overflow: hidden;
            /* Prevent scrolling */
        }

        #bkg-content {
            width: 100%;
            height: 100%;
            position: absolute;
            z-index: 1;
            opacity: 0%;
        }

        #bkg {
            width: 100%;
            height: 100%;
            display: grid;
            gap: 3px;
            position: absolute;
            z-index: 2;
        }

        .pixel {
            width: 9px;
            height: 9px;
        }
    </style>
</head>

<body>
    <canvas id="bkg-content"></canvas>
    <div id="bkg"></div>
    <script src="./script.js"></script>
</body>

</html>