Hey folks, I’m wiring up a little canvas “sparkline trail” toy on a marketing page, and I’m trying to keep it as a lazy-loaded extra so it doesn’t bloat the main bundle. The failure mode I keep hitting is pulling in a helper (noise/random) and suddenly my chunk size jumps way more than the code deserves.
export async function mountTrail(canvas) {
const { makeRng } = await import('./rng.js'); // keep optional
const ctx = canvas.getContext('2d');
const rng = makeRng(1337);
const particles = new Array(240).fill(0).map(() => ({ x: 0, y: 0, vx: 0, vy: 0, a: 0 }));
let t = 0;
function frame() {
t++;
ctx.fillStyle = 'rgba(0,0,0,0.12)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const p of particles) {
p.vx += (rng() - 0.5) * 0.6;
p.vy += (rng() - 0.5) * 0.6;
p.vx *= 0.92; p.vy *= 0.92;
p.x = (p.x + p.vx + canvas.width) % canvas.width;
p.y = (p.y + p.vy + canvas.height) % canvas.height;
ctx.fillStyle = 'rgba(120,220,255,0.9)';
ctx.fillRect(p.x | 0, p.y | 0, 1, 1); // pixel-ish trail
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
Neat part is it looks like a drifting pixel trail with basically no state, and the dynamic import lets me ship it only when the canvas is actually visible.
1 Like
That chunk jump smells like the bundler “optimizing” you into a shared chunk: your ./rng.js is small, but if it imports from some general utils barrel (or a package entrypoint), the graph suddenly includes stuff you didn’t mean to pay for.
I’d keep rng.js brutally standalone (no re-exports, no index.ts, no dependency on your app’s shared helpers) and check the analyzer to see what else got pulled into that async chunk. I’ve been burned by a “tiny” random() that was actually coming from a kitchen-sink module and the lazy chunk ballooned for no good reason.
That chunk jump usually isn’t your RNG code, it’s the bundler deciding that rng. js shares some dependency with your main entry, so it hoists a “vendor-ish” shared chunk and now your cute toy pays for it. One dumb-but-effective trick is to make rng. js truly standalone (no imports, no re-exports), even if it means copying 15 lines of PRNG into that file. For tiny canvas flourishes on marketing pages, I’ve ended up doing that kind of “inline the helper” move more than I’d like, just to keep the lazy chunk predictable.
// trail.js
export async function mountTrail(canvas) {
const { makeRng } = await import('./rng.js'); // keep optional
const ctx = canvas.getContext('2d');
const rng = makeRng(1337);
const particles = new Array(240).fill(0).map(() => ({
x: 0, y: 0, vx: 0, vy: 0, a: 0
}));
let t = 0;
function frame() {
t++;
ctx.fillStyle = 'rgba(0,0,0,0.12)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const p of particles) {
p.vx += (rng() - 0.5) * 0.6;
p.vy += (rng() - 0.5) * 0.6;
p.vx *= 0.92;
p.vy *= 0.92;
p.x = (p.x + p.vx + canvas.width) % canvas.width;
p.y = (p.y + p.vy + canvas.height) % canvas.height;
ctx.fillStyle = 'rgba(120,220,255,0.9)';
ctx.fillRect(p.x | 0, p.y | 0, 1, 1); // pixel-ish trail
}
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
// rng.js (keep it standalone to avoid pulling in shared deps)
export function makeRng(seed = 1) {
// tiny PRNG (mulberry32-ish)
let a = seed >>> 0;
return function rng() {
a |= 0;
a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
I found a related kirupa. com article that can help you go deeper into this topic: