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:
@@ -67,14 +67,20 @@ export default function GlobalAppShell({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
//https://codesandbox.io/p/sandbox/kz9de
|
||||
// Attach the global mousemove/mouseup only WHILE resizing (started on the
|
||||
// handle's mousedown via startResizing → isResizing=true) and detach on
|
||||
// mouseup (stopResizing → isResizing=false). Previously these listeners were
|
||||
// attached for the whole app lifetime, so every mouse move over the app ran
|
||||
// the resize handler.
|
||||
// https://codesandbox.io/p/sandbox/kz9de
|
||||
if (!isResizing) return;
|
||||
window.addEventListener("mousemove", resize);
|
||||
window.addEventListener("mouseup", stopResizing);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", resize);
|
||||
window.removeEventListener("mouseup", stopResizing);
|
||||
};
|
||||
}, [resize, stopResizing]);
|
||||
}, [isResizing, resize, stopResizing]);
|
||||
|
||||
const location = useLocation();
|
||||
const isSettingsRoute = location.pathname.startsWith("/settings");
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Button,
|
||||
useMantineColorScheme,
|
||||
} from "@mantine/core";
|
||||
import { useClickOutside, useDisclosure, useWindowEvent } from "@mantine/hooks";
|
||||
import { useClickOutside, useDisclosure } from "@mantine/hooks";
|
||||
import { Suspense } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -57,14 +57,22 @@ function EmojiPicker({
|
||||
[dropdown, target],
|
||||
);
|
||||
|
||||
// We need this because the default Mantine popover closeOnEscape does not work
|
||||
useWindowEvent("keydown", (event) => {
|
||||
if (opened && event.key === "Escape") {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handlers.close();
|
||||
}
|
||||
});
|
||||
// We need this because the default Mantine popover closeOnEscape does not work.
|
||||
// Attach the global keydown ONLY while the picker is open (every tree row
|
||||
// renders an EmojiPicker, so an always-on window listener meant ~20-30 idle
|
||||
// keydown handlers firing on each keystroke).
|
||||
useEffect(() => {
|
||||
if (!opened) return;
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
handlers.close();
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
return () => window.removeEventListener("keydown", handleKeydown);
|
||||
}, [opened, handlers]);
|
||||
|
||||
// emoji-mart's built-in autoFocus calls .focus() without preventScroll, which
|
||||
// makes the browser scroll every scrollable ancestor of the search input to
|
||||
|
||||
Reference in New Issue
Block a user