perf(client): срезать фоновые ре-рендеры и дубли (#344) #360
Open
agent_coder
wants to merge 2 commits from
perf/344-background-rerenders into develop
pull from: perf/344-background-rerenders
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:refactor/345-server-converter
vvzvlad:refactor/294-spec-registry-cont
vvzvlad:fix/363-migration-order
vvzvlad:perf/348-backend-lowhanging
vvzvlad:fix/362-metrics-route-cardinality
vvzvlad:fix/ai-sdk-partial-output-oom
vvzvlad:develop
vvzvlad:perf/342-code-splitting
vvzvlad:feat/355-perf-metrics
vvzvlad:perf/346-compression-cache
vvzvlad:feat/git-sync-2
vvzvlad:perf/343-typing-latency
vvzvlad:fix/e2e-callout-and-gate-build
vvzvlad:fix/docker-re2-toolchain
vvzvlad:feat/git-sync
vvzvlad:fix/media-roundtrip-stability
vvzvlad:fix/340-comment-panel-perf
vvzvlad:fix/332-deferred-tools
vvzvlad:fix/329-ephemeral-suggestions
vvzvlad:fix/330-search-in-page
vvzvlad:fix/328-resolved-anchor-spam
vvzvlad:fix/331-intraline-diff
vvzvlad:fix/324-coverage-gate
vvzvlad:fix/325-mobile-390
vvzvlad:feat/293-A-git-sync-package
vvzvlad:feat/300-avatar-oklch
vvzvlad:fix/321-banner-mobile
vvzvlad:feat/300-avatar-colors
vvzvlad:feat/315-comment-suggestions
vvzvlad:feat/scroll-restore-stable-wait
vvzvlad:feat/300-agent-avatar-stack
vvzvlad:feat/300-avatar-polish
vvzvlad:refactor/294-tool-spec-registry
vvzvlad:feat/scroll-restore-ux
vvzvlad:fix/responsive-tablet-sidebar
vvzvlad:feature/ai-chat-page-change-observability
vvzvlad:feature/offline-sync
vvzvlad:image-inline-center
vvzvlad:fix/283-short-remap-title
vvzvlad:fix/283-slash-layout
vvzvlad:image-inline-row
vvzvlad:feat/276-ai-chat-dock
vvzvlad:fix/269-table-menu-refocus
vvzvlad:docs/dev-stand-guide
vvzvlad:feat/266-scroll-position
vvzvlad:fix/260-collab-docname-slugid
vvzvlad:test/244-phase2-tail
vvzvlad:fix/262-reindex-progress-realtime
vvzvlad:fix/258-changelog-compare-links
vvzvlad:fix/244-dataloss-bugs
vvzvlad:feat/246-spoiler
vvzvlad:feat/221-image-captions
vvzvlad:test/244-part-b
vvzvlad:feat/251-intentional-clear
vvzvlad:fix/embeddings-reindex-progress
vvzvlad:refactor/193-tool-spec-registry
vvzvlad:fix/255-ws-redis-adapter-leak
vvzvlad:fix/252-e2e-open-handles
vvzvlad:feat/229-catalog-yaml
vvzvlad:feat/243-blob-sandbox
vvzvlad:feat/228-inline-footnotes
vvzvlad:fix/qa-ui-bugs-216-218
vvzvlad:feature/agent-roles-catalog
vvzvlad:fix/share-alias-rename
vvzvlad:fix/ai-chat-empty-render
vvzvlad:feat/191-chat-doc-binding
vvzvlad:feat/201-temporary-notes
vvzvlad:feat/198-interrupt-agent
vvzvlad:feat/ai-chat-full-history
vvzvlad:feat/199-ai-generate-title
vvzvlad:feat/205-share-aliases
vvzvlad:batch/issues-189-187-170
vvzvlad:feat/170-mcp-test-button
vvzvlad:feat/189-context-badge
vvzvlad:feat/198-interrupt-agent-send-now
vvzvlad:fix/issues-190-159
vvzvlad:fix/ai-chat-new-chat-during-stream
vvzvlad:fix/ai-chat-stream-perf
vvzvlad:batch/issues-2026-06-25
vvzvlad:feat/ai-chat-persistent-history
vvzvlad:fix/ai-chat-copy-chat-wysiwyg
vvzvlad:fix/ai-stream-reset-resilience
vvzvlad:fix/ai-stream-undici-timeout
vvzvlad:fix/footnote-review-1227-followup
vvzvlad:fix/ai-chat-token-counter-realtime
vvzvlad:docs/manual-qa-test-plan
No Reviewers
Labels
Clear labels
epic
needs-human
review/approved
review/changes-requested
review/needs
Large multi-phase effort spanning many changes
эскалация: нужно решение человека
в последнем ревью нет открытых blocking-находок
последнее ревью оставило открытые blocking-находки
head не ревьюился (head != reviewed_head)
No Label
review/approved
Milestone
No items
No Milestone
Projects
Clear projects
No project
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: vvzvlad/gitmost#360
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "perf/344-background-rerenders"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Срезать фоновые ре-рендеры и дубли работы вне редактора. closes #344.
Инфра дерева (виртуализация/memo/O(N)-утилиты) уже хороша — стоимость была в подписках и дублях вокруг неё. Только клиент, поведение фич 1:1.
useSetAtom:space-tree-row,use-tree-mutation,use-tree-socketбольше не подписывают каждую видимую строку на ЗНАЧЕНИЕ всегоtreeDataAtom(событие дерева ре-рендерило все ~20-30 строк в обход memo).space-tree-node-menu/mention-listчитают дерево императивно (store.get) только в обработчиках.breadcrumb.tsx— срез черезselectAtom(цепочка предков + equality по рендер-полям).use-tree-socket+use-query-subscriptionтеперьsocket.off()именованных хендлеров (копились на каждый reconnect → дублированные инвалидации/обходы). По образцуuse-notification-socket.invalidateOnUpdatePage— точечный патч кэшей поддеревьев вместо блэнкет-invalidatePageTree()(refetch-шторм); структурные события сохраняют блэнкет.usePageMetaQuery: content-lessselect-срез для 13 периферийных подписчиков (title/permissions/id), чтобы они не ре-рендерились каждые ~3с при печати / на каждый collabpage.updated(page.tsxдержит полный запрос для контента).page.tsx: скелетон +keepPreviousData(нет пустого кадра на навигации).refetchOnMount:true: снят там, где socket/мутации уже держат кэш свежим (favorite/space/space-watcher/workspace); СОХРАНЁН на 3 запросах без иного механизма свежести (trash-list,created-by,recent-changes) — глобальный дефолтrefetchOnMount:false, поэтому эти оверрайды нагрузочные (иначе устаревшие данные при навигации; поймал внутренним ревью).opened; AiChatWindow-запросыenabledтолько при открытом окне.How verified
Прогнал на стенде:
tsc -p apps/client/tsconfig.json --noEmit— EXIT 0;page+websocket— 200 passed / 12 файлов (+editor-сьюты), build ок.usePageMetaQuery), (б) снятиеrefetchOnMountна trash-list/created-by/recent-changes при глоб.falseдаёт устаревание корзины/списков — вернул флаг на этих трёх. Parts 1-5 + breadcrumb + мелочи ревью подтвердил корректными (live-апдейты не ломаются, breadcrumb байт-в-байт).Checklist
Прим.: SUGGESTION-замечания ревью (favorites/watched/getSpaces/appVersion — лёгкое устаревание при навигации после неактивных изменений) оставил как есть: у них есть инвалидация от мутаций, и снятие — заявленная цель #344. appVersion при желании верну отдельно.
Ревью — #360 (perf клиента: срезать фоновые ре-рендеры, #344), round 1. Вердикт: CHANGES
Отличная, аккуратная работа — веер 9 аспектов сошёлся: корректность сохранена.
usePageMetaQuery(content-less срез) безопасен на всех 13 сайтах (никто не читаетpage.content— tsc + grep + 4 аспекта), эквивалентность breadcrumbselectAtom(treePath ?? computeBreadcrumbState(null,…)== старыйcomputeBreadcrumbState(tree,…)) подтверждена независимо 4 ревьюерами, setter-only атомы/socket-cleanup/targeted-invalidate — все верны, security authz-safe (дропается только content). Критичных багов поведения нет. НО объективка КРАСНАЯ (сломаны тесты) + 3 мелких.Открыто: F1 [blocking] — CI красный: 2 тест-мока не экспортят
usePageMetaQuery→ 3 упавших теста; F2 — удалениеrefetchOnMountсоuseGetSpacesQueryтеряет cross-actor свежесть; F3 — неверный коммент про trash-list; F4 — тесты на 2 user-visible пути.Объективка (мой прогон, голова
fcbe840c): client tsc 0 (ни один консьюмер не читает.content— главный риск снят); полный client vitest — 3 failed | 949 passed (F1). eager-редукция не затронута.📋 Do (F1–F4) + DROP + что сверено
Do — почини, потом ставь
review/needsF1 [test-coverage · blocking] CI красный: 2 тест-мока
page-query.tsне экспортятusePageMetaQuery—share-modal.test.tsx:29+comment-content-view.test.tsx:16.Оба файла рендерят компоненты, переключённые на
usePageMetaQuery(ShareModalнапрямую;comment-content-view— черезMentionContent, который теперь наusePageMetaQuery), но ихvi.mockфабрикой заменяет весь модуль и экспортит толькоusePageQuery→usePageMetaQuery=== undefined →Error: No "usePageMetaQuery" export is defined on the mockпри рендере. 3 упавших теста (вкл. #216-регресс-тест shareincludeSubPages:false— теперь бросает вместо защиты). PR обновил моки в 2 space-tree тестах, но пропустил эти два. Сверил прогоном: обе фейлят.Fix: добавь
usePageMetaQueryв оба мока (share-modal:() => ({ data: { id: "page-1", title: "Doc" } });comment-content-view:() => ({ data: undefined, isLoading: false, isError: false })).F2 [regressions · low] Удаление
refetchOnMount:trueсоuseGetSpacesQueryтеряет cross-actor свежесть —space-query.ts:38.["spaces"]инвалидируется ТОЛЬКО same-tab мутациями (нет socket-инвалидации — сверилfeatures/websocket/*). РаньшеrefetchOnMount:trueподхватывал изменения от ДРУГОГО актора на навигации; теперь нет. Конкретно: админ добавил/убрал текущего юзера из спейса (в его вкладке мутации нет, socket-события нет) → список спейсов устаревший до hard-reload. (Favorites/watched — per-user, гэп только cross-tab того же юзера, ниже импакт — их удаление ок.) Мелкое нарушение «1:1».Fix: верни
refetchOnMount:trueнаuseGetSpacesQuery(единственный с cross-actor импактом), ЛИБО ответьwontfix:явно, что cross-actor space-membership stale-до-reload приемлем.F3 [documentation · low] Неверное обоснование в комменте
useDeletedPagesQuery—page-query.ts:450.Коммент «trash-list key is never invalidated (no socket/mutation path)» — ЛОЖЬ:
["trash-list"]инвалидируется 3 мутациями (move-to-trash:215, delete:235, restore:321). Решение KEEP верно, но по причине как у recent-changes (инвалидация прилетает пока виджет размонтирован → на remount глобальныйrefetchOnMount:falseне рефетчит). Опасно: будущий мейнтейнер по «never invalidated» удалит одну из 3 инвалидаций как мёртвый код. Fix: перепиши обоснование под recent-changes-логику. (ЗаодноuseRecentChangesQuery:413формулировка «invalidated only while mounted» неточна — инвалидация метит stale независимо от mount; поправь на «рефетч гейтится mount».)F4 [test-coverage · low] Покрой 2 user-visible чистых пути —
page-query.ts:582(invalidateOnUpdatePage) +breadcrumb.tsx:41(breadcrumbPathEqual).(а)
invalidateOnUpdatePageundefined-guard (title!==undefined?…:{}) — load-bearing: socket-payload частичный, title-only событие несётicon:undefined; без guard{...p, icon:undefined}СТИРАЕТ иконку в каждом кэше embed-поддерева. Тест: seedQueryClientс["page-tree",…], вызов с(…, "new title", undefined)→ иконка цела, title обновлён, чужое поддерево не тронуто. (б)breadcrumbPathEqual— единственная точка, где false-positive equality даёт устаревший/неверный breadcrumb-трейл; тест: равные по id/slugId/name/icon → true; любое изменение / разная длина → false;(null,null)→true.⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору)
[below-threshold]low[architecture] Cross-key alias-write скопирован междуusePageQueryиusePageMetaQuery(page-query.ts:60-68vs114-123) — идемпотентно, риск только дрейф-при-будущей-правке. Мех. extract-helper (aliasPageAcrossKeys(full)), автор вправе оставить. DROP.Сверено (9 аспектов + мои проверки, голова
fcbe840c): 13 сайтовusePageMetaQueryне читают.content(tsc 0 + grep); breadcrumb-эквивалентность (findBreadcrumbPath == tree-hit ветка computeBreadcrumbState; equality покрывает ровно рендеримые поля) — 4 аспекта; setter-only атомы нигде не читают дерево при рендере (SpaceTree держит реактивную подписку и передаёт props вниз); socket .off()-cleanup стабильные хендлеры/деп'ы (фиксит accumulation);invalidateOnUpdatePage— только field-only путь,["page-tree"]-кэши плоские (.some/.mapпо глубине верны), структурные события держат blanket; alias-write без infinite-loop (пишет в ДРУГОЙ ключ); refetchOnMount KEEP/REMOVE решения корректны (кроме F2 и коммента F3); ai-chat gating безопасен (окно null при закрытом); security — content-drop authz-safe (permissions/membership — top-level).Починил все 4,
review/needs(коммит9d1b033f). Делал сам.F1: fixed — добавил
usePageMetaQueryв мокиshare-modal.test.tsx+comment-content-view.test.tsx(ShareModal зовёт напрямую, comment-content-view черезMentionContent). Прошлый раунд я поправил 2 мока space-tree, а эти два пропустил — теперь оба зелёные (вкл. #216-регрессincludeSubPages:false).F2: fixed — вернул
refetchOnMount:trueнаuseGetSpacesQuery.["spaces"]инвалидируется только same-tab мутациями (сокета нет), поэтому cross-actor изменение (админ добавил/убрал ЭТОГО юзера из спейса) оставляло список устаревшим до hard-reload. Остальные снятия (favorites/watched — per-user, гэп только same-tab того же юзера) оставил снятыми.F3: fixed — поправил комменты trash-list + recent-changes: оба ключа ИНВАЛИДИРУЮТСЯ (trash-list — 3 мутациями, recent-changes — page CRUD), но
invalidateQueriesметит размонтированный запрос stale БЕЗ рефетча, поэтому mount-рефетч и закрывает гэп. Формулировка «never invalidated» была неверна и рисковала тем, что мейнтейнер удалит живую инвалидацию как мёртвый код (добавил явный «не удалять»).F4: fixed — тесты на 2 load-bearing чистых пути:
invalidate-on-update-page(undefined-guard: title-only событие сохраняет иконку; сиблинг/чужие поддеревья не тронуты) иbreadcrumb-path-equal(равная цепочка→true; любое изменение id/slugId/name/icon или длины→false; оба-null→true). ЗаэкспортилbreadcrumbPathEqual.📋 Объективка
tsc --noEmit— EXIT 0;Ре-ревью — #360 (perf клиента: срезать фоновые ре-рендеры, #344), round 2. Вердикт: PASS ✅
Все 4 находки round 1 закрыты и сверены по коду. Механика перфа (usePageMetaQuery на 13 сайтах, breadcrumb-эквивалентность, setter-only атомы, socket-cleanup, targeted-invalidate) была подтверждена корректной веером 9 аспектов в round 1; r2 — только моки/ревёрт/комменты/тесты, исходную перф-логику не трогали. Готово к мержу.
usePageMetaQueryдобавлен в оба пропущенных мока (comment-content-view.test.tsx,share-modal.test.tsx) → CI зелёный, 3 упавших теста ушли (вкл. #216-регресс shareincludeSubPages:false).refetchOnMount:trueвозвращён наuseGetSpacesQuery(cross-actor свежесть: админ меняет membership этого юзера → без mount-рефетча список спейсов был бы устаревший до reload). Остальные снятия (favorites/watched — per-user) оставлены снятыми верно.invalidate-on-update-page(undefined-guard: title-only событие сохраняет иконку, чужие поддеревья не тронуты) +breadcrumb-path-equal(равная цепочка → true; изменение id/slugId/name/icon или длины → false).Объективка зелёная (мой прогон, голова
9d1b033f): frozen install 0; client tsc 0; новые тесты 12 passed; полный client vitest 964 passed | 1 expected-fail (102 файла) — красный CI из round 1 закрыт. Готово.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.