perf: reduce GC pressure in render loops

- Pre-compute star RGB components to avoid 3 string.replace() calls per
  star per frame (~40K string allocs/sec eliminated)
- Cache floor gradient object; rebuild only on canvas resize
- Batch CAVA bar DOM updates through requestAnimationFrame so SSE events
  never trigger mid-frame style mutations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ltadeu6 2026-06-07 13:07:33 -03:00
parent e7a95c8c86
commit 1957fda57e
No known key found for this signature in database
GPG key ID: FB9FDAB6B6F3418D

View file

@ -794,16 +794,19 @@ const ctx = canvas.getContext('2d');
const STAR_COUNT = 220; const STAR_COUNT = 220;
const stars = []; const stars = [];
const STAR_COLORS = [[255,221,170],[204,216,255],[238,238,255]];
function initStars() { function initStars() {
stars.length = 0; stars.length = 0;
for (let i = 0; i < STAR_COUNT; i++) { for (let i = 0; i < STAR_COUNT; i++) {
const c = STAR_COLORS[Math.random() < 0.15 ? 0 : Math.random() < 0.5 ? 1 : 2];
stars.push({ stars.push({
x: Math.random() * canvas.width, x: Math.random() * canvas.width,
y: Math.random() * canvas.height * 0.62, y: Math.random() * canvas.height * 0.62,
r: Math.random() * 1.6 + 0.2, r: Math.random() * 1.6 + 0.2,
phase: Math.random() * Math.PI * 2, phase: Math.random() * Math.PI * 2,
freq: 0.4 + Math.random() * 1.2, freq: 0.4 + Math.random() * 1.2,
color: Math.random() < 0.15 ? '#ffddaa' : (Math.random() < 0.5 ? '#ccd8ff' : '#eeeeff'), cr: c[0], cg: c[1], cb: c[2],
}); });
} }
} }
@ -818,13 +821,10 @@ function drawStars(ts) {
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
const t = ts * 0.001; const t = ts * 0.001;
stars.forEach(s => { stars.forEach(s => {
const alpha = 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase)); const alpha = (0.25 + 0.75 * (0.5 + 0.5 * Math.sin(t * s.freq + s.phase))).toFixed(2);
ctx.beginPath(); ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fillStyle = s.color.replace(')', `, ${alpha.toFixed(2)})`) ctx.fillStyle = `rgba(${s.cr},${s.cg},${s.cb},${alpha})`;
.replace('rgb', 'rgba').replace('#ccd8ff', `rgba(204,216,255,${alpha.toFixed(2)})`)
.replace('#eeeeff', `rgba(238,238,255,${alpha.toFixed(2)})`)
.replace('#ffddaa', `rgba(255,221,170,${alpha.toFixed(2)})`);
ctx.fill(); ctx.fill();
}); });
requestAnimationFrame(drawStars); requestAnimationFrame(drawStars);
@ -845,6 +845,8 @@ const V_LINES = 24;
const H_LINES = 24; const H_LINES = 24;
let floorScroll = 0; let floorScroll = 0;
let lastFloorTs = 0; let lastFloorTs = 0;
let floorFadeGrad = null;
let floorFadeGradH = 0;
function resizeFloor() { function resizeFloor() {
floorCanvas.width = window.innerWidth; floorCanvas.width = window.innerWidth;
@ -891,11 +893,14 @@ function drawFloor(ts) {
floorCtx.stroke(); floorCtx.stroke();
} }
// Fade near horizon to hide sub-pixel lines // Fade near horizon to hide sub-pixel lines (gradient cached, only rebuilt on resize)
const fade = floorCtx.createLinearGradient(0, 0, 0, H * 0.3); if (!floorFadeGrad || floorFadeGradH !== H) {
fade.addColorStop(0, '#000814'); floorFadeGradH = H;
fade.addColorStop(1, 'rgba(0,8,20,0)'); floorFadeGrad = floorCtx.createLinearGradient(0, 0, 0, H * 0.3);
floorCtx.fillStyle = fade; floorFadeGrad.addColorStop(0, '#000814');
floorFadeGrad.addColorStop(1, 'rgba(0,8,20,0)');
}
floorCtx.fillStyle = floorFadeGrad;
floorCtx.fillRect(0, 0, W, H * 0.3); floorCtx.fillRect(0, 0, W, H * 0.3);
requestAnimationFrame(drawFloor); requestAnimationFrame(drawFloor);
@ -983,21 +988,28 @@ function detectBeat(vals) {
bassEma = bassEma * 0.96 + bass * 0.04; bassEma = bassEma * 0.96 + bass * 0.04;
} }
// Called with semicolon-separated CAVA values (0100 per bar) // Called with semicolon-separated CAVA values (0100 per bar) — SSE may fire >60fps,
function updateBars(raw) { // so we buffer and flush once per animation frame to avoid mid-frame DOM thrashing.
if (!isPlaying) return; let pendingCava = null;
const vals = raw.split(';').map(v => Math.min(100, parseInt(v, 10) || 0));
detectBeat(vals);
// VU meter (low bars) function updateBars(raw) {
pendingCava = raw;
}
function flushBars() {
if (pendingCava && isPlaying) {
const vals = pendingCava.split(';').map(v => Math.min(100, parseInt(v, 10) || 0));
detectBeat(vals);
vuBars.forEach((bar, i) => { vuBars.forEach((bar, i) => {
const v = vals[i] || 0; const v = vals[i] || 0;
bar.style.height = Math.max(3, (v / 100) * MAX_BAR_H) + 'px'; bar.style.height = Math.max(3, (v / 100) * MAX_BAR_H) + 'px';
bar.style.opacity = 0.3 + 0.7 * (v / 100); bar.style.opacity = 0.3 + 0.7 * (v / 100);
}); });
pendingCava = null;
}
requestAnimationFrame(flushBars);
} }
requestAnimationFrame(flushBars);
/* ============================================================ /* ============================================================
MUSIC-LIGHT THEME (album color) MUSIC-LIGHT THEME (album color)