From fdd3e74e2afc6843906bc953b220f31c537cd389 Mon Sep 17 00:00:00 2001 From: dhaatrik <90041791+dhaatrik@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:09:01 +0000 Subject: [PATCH] perf(Game): replace push with indexed array assignments in wind rendering In high-frequency rendering loops like `drawWindVectors`, `Array.prototype.push()` adds significant function call overhead. This commit replaces `.push()` with direct indexed array assignments (`arr[idx++] = val`) when populating the wind batch arrays to improve rendering performance and reduce GC pressure. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .jules/bolt.md | 3 +++ src/core/Game.ts | 41 +++++++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index bec1f7a..e61bfee 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -4,3 +4,6 @@ ## 2024-05-30 - Caching window.speechSynthesis.getVoices() **Learning:** `window.speechSynthesis.getVoices()` is asynchronous and may return an empty array initially. Caching it too early without checking its length can result in a permanent cache miss. **Action:** Always verify `voices.length > 0` before storing the result in a cache to ensure voices are actually loaded. +## 2024-06-25 - Array.push Overhead in Render Loops +**Learning:** In high-frequency rendering loops (e.g., `drawWindVectors` in `Game.ts` which processes hundreds of vertices per frame), `Array.prototype.push()` introduces significant function call overhead compared to direct indexed assignment (`arr[idx++] = val`). +**Action:** Replace `.push()` with direct index assignments when populating batch arrays in performance-critical paths, especially when the arrays are cleared (`length = 0`) and reused every frame. diff --git a/src/core/Game.ts b/src/core/Game.ts index 46d948a..1167913 100644 --- a/src/core/Game.ts +++ b/src/core/Game.ts @@ -619,16 +619,10 @@ export class Game { const tempWind = { speed: 0, direction: 0 }; const screenX = 50 / this.ZOOM; // Draw on left side (scaled) - // Batches for each color category (stores vertices) - // Format: [x1, y1, x2, y2, ...] - this._windLowBatch.length = 0; - this._windMedBatch.length = 0; - this._windHighBatch.length = 0; - - // Text batch: [speed, x, y] (SoA) - this._windTextStrings.length = 0; - this._windTextX.length = 0; - this._windTextY.length = 0; + let lowIdx = 0; + let medIdx = 0; + let highIdx = 0; + let textIdx = 0; for (let y = startY; y < camY + this.height / this.ZOOM; y += step) { const alt = (this.groundY - y) / PIXELS_PER_METER; @@ -658,18 +652,33 @@ export class Game { const ty = vx * s + vy * c + screenY; // Add to appropriate batch - if (speed < 10) this._windLowBatch.push(tx, ty); - else if (speed < 30) this._windMedBatch.push(tx, ty); - else this._windHighBatch.push(tx, ty); + if (speed < 10) { + this._windLowBatch[lowIdx++] = tx; + this._windLowBatch[lowIdx++] = ty; + } else if (speed < 30) { + this._windMedBatch[medIdx++] = tx; + this._windMedBatch[medIdx++] = ty; + } else { + this._windHighBatch[highIdx++] = tx; + this._windHighBatch[highIdx++] = ty; + } } // Add text info (offset by 10, 15 relative to arrow center) - this._windTextStrings.push(`${speed.toFixed(0)} m/s`); - this._windTextX.push(screenX + 10); - this._windTextY.push(screenY + 15); + this._windTextStrings[textIdx] = `${speed.toFixed(0)} m/s`; + this._windTextX[textIdx] = screenX + 10; + this._windTextY[textIdx] = screenY + 15; + textIdx++; } } + this._windLowBatch.length = lowIdx; + this._windMedBatch.length = medIdx; + this._windHighBatch.length = highIdx; + this._windTextStrings.length = textIdx; + this._windTextX.length = textIdx; + this._windTextY.length = textIdx; + // Render batches const drawBatch = (points: number[], color: string) => { if (points.length === 0) return;