From 1e7a7abc6494533e890d2baff55561e6725f945c Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 20 Aug 2026 06:13:24 +0200 Subject: [PATCH] perf: group the timeline's lanes in one pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacking by game rebuilt a lane's whole array for every row it added, which is quadratic in the lane's length. That is cheap at three events and not at a reader with fourteen games switched on and the future plotted — and it is not paid once, because the board re-renders on every clock tick. Appending into the array instead is the same output: `Map` preserves insertion order, so lanes still arrive in the order their first row did and the rows inside one keep the order they were given, which is what the existing tests pin. Co-Authored-By: Claude Opus 5 (1M context) --- src/client/state/lanes.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/client/state/lanes.ts b/src/client/state/lanes.ts index ec789a6..a0fba5c 100644 --- a/src/client/state/lanes.ts +++ b/src/client/state/lanes.ts @@ -97,9 +97,15 @@ export function timelineLanes( return [{ id: "all", game: null, rows: [...rows].sort(order) }]; } + // Appended into rather than rebuilt per row: the copy-and-reset form this + // replaced was quadratic in a lane's length, and it runs on every render of a + // board that redraws each second. `Map` keeps insertion order, so the lanes + // still arrive in the order their first row did. const byGame = new Map(); for (const row of rows) { - byGame.set(row.event.game, [...(byGame.get(row.event.game) ?? []), row]); + const lane = byGame.get(row.event.game); + if (lane === undefined) byGame.set(row.event.game, [row]); + else lane.push(row); } let lanes = [...byGame];