Compress what the browser downloads, and stop one tile taking the page
**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
This commit is contained in:
@@ -31,6 +31,11 @@ export const searchQueryOptions = () => ({
|
||||
})
|
||||
|
||||
/** The categories, in the order they are offered, with what to draw each as. */
|
||||
//: The most entries one group shows. cmdk scores what is mounted, so this is
|
||||
//: a cap on what is offered rather than on what is searched — and twenty of a
|
||||
//: kind is already more than anybody reads before typing another letter.
|
||||
const GROUP_CAP = 20
|
||||
|
||||
const GROUPS: {
|
||||
category: SearchEntry["category"]
|
||||
label: string
|
||||
@@ -58,8 +63,10 @@ function hint(entry: SearchEntry): string {
|
||||
* The whole index arrives in one fetch and `cmdk` does the matching, so results
|
||||
* narrow as they are typed without a round trip per keystroke.
|
||||
*
|
||||
* ponytail: every entry is rendered and cmdk hides the ones that do not match.
|
||||
* Cap the groups if an installation ever grows big enough to feel it.
|
||||
* cmdk does the matching over what is mounted, so the entries are bucketed by
|
||||
* category once per fetch rather than filtered nine times per keystroke, and
|
||||
* each group is capped — a list nobody can scroll to the end of is not a
|
||||
* better answer than its first twenty.
|
||||
*/
|
||||
export function GlobalSearch({
|
||||
open,
|
||||
@@ -79,6 +86,15 @@ export function GlobalSearch({
|
||||
|
||||
const entries = data ?? []
|
||||
const typing = query.trim().length > 0
|
||||
// One pass over the index rather than one per group — nine passes over
|
||||
// every entry on each keystroke. Not memoised: the component returns early
|
||||
// while closed, so a hook cannot go here, and one pass is already the win.
|
||||
const byCategory = new Map<string, SearchEntry[]>()
|
||||
for (const entry of entries) {
|
||||
const bucket = byCategory.get(entry.category)
|
||||
if (bucket) bucket.push(entry)
|
||||
else byCategory.set(entry.category, [entry])
|
||||
}
|
||||
|
||||
const go = (entry: SearchEntry) => {
|
||||
onOpenChange(false)
|
||||
@@ -144,9 +160,7 @@ export function GlobalSearch({
|
||||
<>
|
||||
<CommandEmpty>Nothing matches that.</CommandEmpty>
|
||||
{GROUPS.map(({ category, label, icon: Icon }) => {
|
||||
const found = entries.filter(
|
||||
(entry) => entry.category === category,
|
||||
)
|
||||
const found = (byCategory.get(category) ?? []).slice(0, GROUP_CAP)
|
||||
if (found.length === 0) return null
|
||||
return (
|
||||
<CommandGroup key={category} heading={label}>
|
||||
|
||||
Reference in New Issue
Block a user