**nginx served the bundle uncompressed and uncacheable.** The base image
ships gzip commented out and nothing set `Cache-Control`, so every load
carried the whole thing and every reload cost a 304 per asset. Measured on
the built image: the entry chunk 838 kB → 311 kB, the Monaco chunk 2.66 MB
→ 832 kB, and the ~50 content-hashed assets are now immutable for a year.
`index.html` is explicitly `no-cache`, since it is what names the rest.
**A widget that throws no longer blanks the screen.** There was one error
boundary in the app, on the root route, so anything that threw replaced
everything including the navigation — on `/view/{name}`, an unattended wall
panel with no way back. Each tile has its own boundary now, and the app
shell has one inside it so a screen that fails leaves the sidebar standing.
`react-error-boundary` was already a dependency and imported nowhere.
**`localStorage` cannot take the app down.** Reaching it raises where the
browser blocks site data, and `setItem` raises once the origin's quota is
full — which the flow editor's node clipboard, carrying whole Python
sources, can genuinely reach. Thrown from a key handler that escaped to
`window.onerror`, which the single root boundary then turned into a blank
page. `lib/safeStorage.ts` is the guarded pair the pre-paint theme script in
`index.html` was already using; a copy too large to store now says so.
Queries default to `staleTime: 5000` — below every poll interval on any
screen, so nothing polls less often than it did, but a route mounting twice
in a few seconds stops refetching everything it touches. Window-focus
refetching is off: the socket pushes what changes and a reconnect
invalidates what it feeds, so a focus event has nothing of its own to say.
Home alone reads about ten queries on every one of those.
Render cost, two that showed up in the audit:
- `LogsPanel` was rendered unconditionally by the dock and decided inside
itself whether to draw, so with the panel *shut* it still subscribed to
the log store and re-filtered five hundred lines per line a flow
published. It returns before any of that now.
- `HealthActivity` subscribed to the whole engine-event array and used one
number from it, so a flapping node re-rendered the component that draws
Home's two uPlot charts — each of which rebuilds its series on every
render by design. It subscribes to that number.
- the global search bucketed the index nine times per keystroke, once per
group. One pass, and each group offers at most twenty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M6hPWS6YEbT1P8LxhhFb2T
41 lines
1.2 KiB
TypeScript
41 lines
1.2 KiB
TypeScript
/**
|
|
* `localStorage` that cannot throw.
|
|
*
|
|
* Reaching `window.localStorage` at all raises `SecurityError` where the
|
|
* browser is set to block site data, and `setItem` raises `QuotaExceededError`
|
|
* once the origin's five megabytes are full — which the flow editor's node
|
|
* clipboard, carrying whole Python sources, can genuinely reach. Either one
|
|
* thrown from an event handler escapes to `window.onerror`, and with a single
|
|
* error boundary at the root that blanks the app.
|
|
*
|
|
* The pre-paint theme script in `index.html` already guards its own read this
|
|
* way; this is the same thing for everything that runs after it.
|
|
*/
|
|
export const safeStorage = {
|
|
get(key: string): string | null {
|
|
try {
|
|
return localStorage.getItem(key)
|
|
} catch {
|
|
return null
|
|
}
|
|
},
|
|
|
|
/** Whether it was actually stored, for the rare caller that wants to know. */
|
|
set(key: string, value: string): boolean {
|
|
try {
|
|
localStorage.setItem(key, value)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
},
|
|
|
|
remove(key: string): void {
|
|
try {
|
|
localStorage.removeItem(key)
|
|
} catch {
|
|
// Nothing to clean up if it was never reachable.
|
|
}
|
|
},
|
|
}
|