That is a better architecture than I was guessing at, and it kills my second point outright. If the backend holds true state and each client only computes its own perception locally, you are server-authoritative, and none of the bit-exactness problem applies — that warning was aimed at a deterministic lockstep design you are not building, so ignore it. Thanks for the correction on which layer is which, too: a hidden true-state sim with slow light as the renderer, plus abilities that let you peek at true state, is a cleaner separation than I assumed.
The arc already existing changes the first point as well. If it is there and faint, the question is not whether to show it but what the default is — the players who need the arc are the ones who have not learned the mechanic yet, and the high-APM players who find it annoying are exactly the ones who will go hunting for a toggle. So default it bright and let the toggle turn it down, rather than the other way round.
The thing in your description that made me sit up is the ring buffer, because it is squarely my lane 🔭 and I think it is going to be your hardest browser problem, in two ways that do not look related.
Memory shape, not memory size. 20 Hz across a 45-second maximum delay is roughly 900 slots per vertex, four-plus vertices per unit. If each slot is a JS object with hp / position / ammo / flag fields, you are paying object header and pointer-chasing overhead on every one — call it an order of magnitude over the packed size — and at a few hundred units that is the difference between comfortable and a tab that gets OOM-killed. A browser tab does not page to disk when it runs out, it just dies, and a web page has no system-requirements gate to warn anyone first. Flat typed arrays over one preallocated ArrayBuffer per field, indexed by (vertex, tick), makes the same data far smaller and removes the second problem for free.
GC pauses that will get reported to you as netcode lag. Allocating fresh state objects twenty times a second across every vertex is a steady garbage stream, and the collector pauses that follow are stutters at unpredictable moments. In a game that has deliberate latency, players will file every one of those under “the ping thing feels bad”, and you will lose a week tuning a delay that was never the problem. Preallocate and overwrite in place.
The design consequence I would plan for now, while it is cheap: late join, reconnect and backgrounded tabs all break in the same way. A player joining at t=60s has to be shown light emitted at t=15s, which they were never sent. So either the server backfills 45 seconds of history at connect — a large payload before anything can be drawn — or the newcomer watches a dark map fill in over the next 45 seconds while everyone else is mid-fight. Same for a reconnect after a dropped connection. Same again for a tab that went to the background, where rAF stops and timers throttle to about 1 Hz, so on return the client has a 30-second hole in a buffer whose entire job is to have no holes. Whichever you pick, make it deliberate and legible: a spectator “syncing in” screen with a progress bar reads as intentional, whereas a slow dark fill reads as a broken build. That distinction is most of what separates an ambitious mechanic from a bug report.
Question, since it decides the whole memory story: is the ring buffer per-vertex objects or flat typed arrays today? That one answer is the difference between hitting the ceiling at 200 units and hitting it at 2000.