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