Skip to content

Work / Trunk / Wiki / Decisions

2026-10-20-edit-state-and-caching

Decisioncanonicalverified 2026-10-20

DECISION.2026-10-20.EDIT-STATE-AND-CACHING

An edit on a canvas reads back only its canvas

Asked for and approved by the owner on 2026-10-20.

Decision

An edit on a page no longer reloads the screen: it reads back that page's canvas into a browser store. A page render works out the draft once, and a read writes only when main moved.

  • A page render works out the draft once. editorValues and the membership check are kept for the length of a render with React cache. A tRPC call is not a render, so a write always reads its own transaction.
  • A read writes only when main moved. A draft on main already at main's head skips its sync (syncDraft in @trunk/history). Only a commit changes main's values, and every commit moves main's head. See branch-verify-merge.
  • An edit on a page reads back only that page's canvas. A Zustand store (apps/web/src/stores/canvas-store.ts) holds each open page's drawn elements and edited properties. Canvas hooks call useCanvasRefresh(), which asks drawing.canvas for that page and puts the answer in the store. The frame already redraws itself from the draft. Layers and the drawn inspector read the store.
  • A full reload stays where the shape of the product changes: a new or deleted page, Review, merge, sync, undo, and anything on a component's canvas, which has no store yet.
  • Folder rules are linted. lib and constants import none of components, hooks, stores or server; server imports none of components, hooks or stores; hooks and stores import no components. Types may cross. knip runs in pnpm verify.

Why

Every edit called router.refresh(). The server then ran every loader of the screen again: one reload of a page in Edit was 212 SQL statements in 17 transactions, about 330 ms on a laptop. Each loader worked out the draft's values again, about 8 times per render, and syncing the draft wrote to it even when main had not moved. Reads still go through withMember (tenant-isolation).

Considered and not done:

  • Rust. The heavy work runs in Chromium; the cost was repeated reads.
  • React Query or SWR. Reads come from server components; a second cache would be a second copy of the same data.
  • Caching the page frame (ETag, LRU). The frame is no-store on purpose, and every edit is a new revision, so a cache would rarely hit.
  • nuqs. The URL helpers are about 150 lines and most of the address is read on the server.
  • Server-Sent Events in place of polling. Polling every 1–3 s is a few lines; SSE needs a long-lived database listener.
  • Undo in the browser. Undo restores a snapshot of the draft, which covers every kind of change. It stays on the server.

Impact

  • One reload of a page in Edit: 212 statements → 113. A rename in Layers is one save and one canvas read, with no reload.
  • A new edit hook on a canvas calls useCanvasRefresh(), never router.refresh().
  • A component's canvas keeps its reload: measured on 2026-10-20 it is 87 statements and about 160 ms, and a store would copy most of the component inspector's data into the browser.
  • A move to another screen shows a loading screen at once (pages, components, Journey, Review, Compare, Branches, Versions); picking inside one screen does not, so a Styles token or an Analysis section never blanks the panel just clicked.

Status

Active.

Sources