perf(client): cut background re-renders + duplicate work (#344)
Outside the editor the UI did background work on every tree event, socket reconnect, and navigation. Tree infra (virtualization/memo/O(N) utils) was already good — the cost was in the subscriptions and duplicates around it. Client-only; behavior 1:1. - Setter-only atom subscriptions → useSetAtom: space-tree-row, use-tree-mutation, use-tree-socket no longer subscribe every visible row to the WHOLE treeDataAtom value (a tree event re-rendered all ~20-30 rows, bypassing the DocTreeRow memo). space-tree-node-menu / mention-list read the tree imperatively (store.get) in their handlers only. breadcrumb.tsx uses a selectAtom slice (ancestor chain + field equality) instead of the whole-tree subscription. - Socket handler cleanup (BUG): use-tree-socket + use-query-subscription now socket.off() their named handlers on cleanup (were accumulating listeners on every reconnect → duplicated invalidations/tree-walks). Mirrors use-notification-socket. - Field-update tree path: invalidateOnUpdatePage does a pointwise patch of the cached embed subtrees instead of a blanket invalidatePageTree() (refetch storm); structural events keep the blanket invalidate. - usePageMetaQuery: a content-less select slice for the 13 peripheral subscribers that read only title/permissions/id, so they stop re-rendering every ~3s while typing / on every collab page.updated (page.tsx keeps the full query for content). - page.tsx: skeleton + placeholderData keepPreviousData (no blank flash on nav). - Removed refetchOnMount:true where socket/mutation invalidation already keeps the cache fresh (favorite/space/space-watcher/workspace). KEPT it on the 3 queries with NO other freshness path (trash-list, created-by, recent-changes) — the global default is refetchOnMount:false, so those overrides are load-bearing. - Small: resize mousemove/up attached only while dragging; per-row emoji-picker keydown gated on `opened`; AiChatWindow queries enabled only when the window is open. Gate: client tsc 0, client vitest page+websocket 200 passed (+editor suites), build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useSetAtom, useStore } from "jotai";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { ActionIcon, Menu, rem } from "@mantine/core";
|
||||
@@ -52,7 +52,11 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) {
|
||||
const clipboard = useClipboard({ timeout: 500 });
|
||||
const { spaceSlug } = useParams();
|
||||
const { handleDelete } = useTreeMutation(node.spaceId);
|
||||
const [data, setData] = useAtom(treeDataAtom);
|
||||
// Setter-only: the tree value is read only imperatively inside the duplicate
|
||||
// handler (via `store` below), never at render, so useSetAtom avoids
|
||||
// re-rendering every row's NodeMenu on any tree event.
|
||||
const setData = useSetAtom(treeDataAtom);
|
||||
const store = useStore();
|
||||
const emit = useQueryEmit();
|
||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||
useDisclosure(false);
|
||||
@@ -125,8 +129,8 @@ export function NodeMenu({ node, canEdit }: NodeMenuProps) {
|
||||
try {
|
||||
const duplicatedPage = await duplicatePage({ pageId: node.id });
|
||||
|
||||
// figure out parent + insertion index
|
||||
const siblings = treeModel.siblingsOf(data, node.id);
|
||||
// figure out parent + insertion index (read the live tree imperatively)
|
||||
const siblings = treeModel.siblingsOf(store.get(treeDataAtom), node.id);
|
||||
const parentId = siblings?.parentId ?? null;
|
||||
const currentIndex = siblings?.index ?? 0;
|
||||
const newIndex = currentIndex + 1;
|
||||
|
||||
Reference in New Issue
Block a user