a day ago
Title: Dashboard pins a CPU core: infinite re-render loop in NotificationCenter (goneProjectIds / goneEnvironmentKeys effect)
Summary
Any open Railway dashboard tab keeps one CPU core busy all the time (~100% CPU on the tab’s renderer process; the laptop reaches 97°C). This happens on every dashboard page, including /dashboard and project and service views, because the bell / NotificationCenter in the header causes it. There’s no user interaction and network traffic is minimal, so the cause is a client-side render loop.
Environment
Browser: Brave (Chromium-based), macOS - version:
Date observed: 2026-11-12
Bundle: assets/NotificationCenter-BKZqIN3I.js
Steps to reproduce
Open any page of the Railway dashboard while logged in.
Open the browser Task Manager and watch the tab’s CPU.
Expected: near-zero CPU while idle.
Actual: about 100% CPU on the renderer process for as long as the tab stays open.
Root cause (from a Performance trace and runtime inspection)
The main thread is busy about 43% of a 58 s trace. Nearly all of that is React’s scheduler work loop (4,927 tasks of ~5 ms each), and the hottest app frames are in NotificationCenter-BKZqIN3I.js.
React commits about 200 times per second. The only state that changes between commits is a Jotai useAtomValue in the NotificationCenter component (minified mn). It subscribes to the unread-notifications derived atom (get(Q).filter(n => n.readAt == null)).
That atom depends on a primitive atom shaped like { goneProjectIds: Set, goneEnvironmentKeys: Set }. This atom is written about 18,000 times per second, every time with a new object containing new empty Sets.
All writes come from a single useEffect in NotificationCenter-BKZqIN3I.js:1:18089 (the effect that uses projectsByIds and computes the gone project and environment keys).
The loop goes like this: the effect runs and writes a new {goneProjectIds, goneEnvironmentKeys} object. The derived atoms recompute and return new arrays, the component re-renders, the effect runs again, and so on forever. The value never actually changes, only its identity does.
Verification
I patched the store in the live tab so that writes to that atom are skipped when both Sets have the same contents as the current value. React scheduler activity dropped from ~200 tasks/s to 0 and CPU went back to idle. Only one write was skipped, so a single redundant write is enough to keep the loop going.
Suggested fix
Either of these breaks the loop:
Compare Set contents before calling set in that effect, and skip the write if nothing changed.
Derive gone IDs with useMemo or a derived atom instead of writing them from an effect, and check the effect’s dependency array for values that get a new identity on every render.
I can share the Performance trace (.json.gz) on request.
0 Threads mention this feature
0 Replies
