perf(client): route + component code-splitting — eager 3.5МБ→1.12МБ (#342) #354
Open
agent_coder
wants to merge 3 commits from
perf/342-code-splitting into develop
pull from: perf/342-code-splitting
merge into: vvzvlad:develop
vvzvlad:main
vvzvlad:feat/371-roles-catalog
vvzvlad:feat/370-page-versioning
vvzvlad:refactor/345-server-converter
vvzvlad:feat/196-multi-cursor
vvzvlad:test/351-generative-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:perf/344-background-rerenders
vvzvlad:develop
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#354
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/342-code-splitting"
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
Клиентский code-splitting: убрать тяжёлый код со стартового пути. closes #342.
Раньше весь код сидел в eager-графе (App.tsx статически импортил все 28 роутов; редактор тянул TipTap + KaTeX + ~45 грамматик lowlight + drawio; posthog + AI SDK грузились всем) — на
/loginкачался и компилировался весь редактор. Только клиент, поведение фич 1:1, меняется только МОМЕНТ загрузки кода.Результат (прод-сборка): eager-JS 3.5МБ → ~1.12МБ, entry-чанк 1920КБ → 552КБ; KaTeX (250КБ) и движок TipTap (~586КБ) теперь ленивые чанки вне стартового пути.
React.lazy+Suspense(редакторPage, всеsettings/*, share, space/home). Auth/redirect/cold-start роуты — eager. Suspense внутриLayout/ShareLayoutвокругOutlet— shell остаётся смонтированным.math-*-lazy) + ленивый drawio (drawio-*-lazy) по образцу mermaid/excalidraw, у каждого локальный Suspense с плейсхолдером размера узла.isCloud() && isPostHogEnabled; self-hosted не качает.React.lazy, монтируется по первому открытию и остаётся смонтированным (живой AI-стрим не рвётся); закрытый рендеритnull.Editor→import type;Asideленивый;config.tssanitizeUrlиuse-clipboardexecCommandCopy→ клиент-локальныеsrc/lib/{sanitize-url,copy-to-clipboard}.ts(побайтово идентичны оригиналам из editor-ext, но без top-level@tiptapиз бочки);WebSocketStatus→ строковый литерал"connected", который атом статуса и так хранит.vendor-katex(TipTap/PM/Yjs намеренно НЕ группированы — группировка тащила движок в eager).listLanguages()/подсветка синхронны, отложенная регистрация изменила бы поведение; route-split уже убирает их со старта (это и была жалоба).How verified
Прогнал на стенде (CI-условия, изолированный frozen install):
pnpm --filter client build— успех; eager-JS 1.12МБ (13 файлов), entry 552КБ; KaTeX НЕ вindex.htmlmodulepreload; движок TipTap — ленивый.tsc -p apps/client/tsconfig.json --noEmit— EXIT 0.pnpm install --frozen-lockfile— EXIT 0 (добавил@braintree/sanitize-urlпрямой зависимостью client + регенерировал лок — иначе CI красный на install).Checklist
Everything sat in the eager startup graph (App.tsx statically imported all 28 routes; the editor pulled TipTap + KaTeX + ~45 lowlight grammars + drawio; posthog + AI SDK loaded for everyone) — a /login visitor downloaded+compiled the whole editor. Client-only; functionality 1:1, only WHEN code loads changed. Result (prod build): eager JS 3.5MB -> ~1.12MB, entry 1920KB -> 552KB; KaTeX (250KB) and the TipTap engine (~586KB) are now lazy chunks, off the startup path. - App.tsx: route-level React.lazy + Suspense (editor Page, all settings/*, share, space/home routes). Auth/redirect/cold-start routes stay eager. Suspense lives inside Layout/ShareLayout around the Outlet so the shell stays mounted. - Lazy KaTeX node views (math-inline-lazy/math-block-lazy) + lazy drawio (drawio-view-lazy/drawio-menu-lazy), mirroring mermaid/excalidraw, each with a node-sized Suspense placeholder so a slow chunk can't crash the editor. - posthog-js is now a conditional dynamic import under the unchanged isCloud() && isPostHogEnabled gate — self-hosted never downloads it. - AiChatWindow is React.lazy, mounted on first open and kept mounted (a live AI stream isn't torn down); renders null while closed (identical behavior). - Cut eager TipTap pulls from always-loaded shell modules: editor-atoms / global-bridge Editor -> import type; Aside lazily loaded (page routes only); config.ts sanitizeUrl and use-clipboard execCommandCopy moved to client-local src/lib/{sanitize-url,copy-to-clipboard}.ts (byte-identical to the editor-ext originals, dropping the barrel's top-level @tiptap import); WebSocketStatus import replaced with the "connected" literal the status atom already stores. - vite.config.ts: a vendor-katex chunk group (TipTap/PM/Yjs intentionally NOT grouped — grouping dragged the engine eager; documented in the config). - lowlight grammar registration is left inside the (now-lazy) editor chunk: listLanguages()/highlighting are synchronous, so deferring registration would change behavior for marginal in-chunk gain — the route split already removes it from startup, which was the complaint. Gate: client build succeeds, tsc --noEmit clean, frozen install EXIT 0 (added @braintree/sanitize-url as a direct client dep + regenerated the lock). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>review/needs. Внутренний цикл: 1 проход + мой ревью-сабагент — поймал и починил CI-блокер (добавленный dep@braintree/sanitize-urlне был в локе →frozen-lockfileпадал; регенерировал лок, теперь EXIT 0); высокорисковые правки сверил как поведенчески эквивалентные.Что сверял отдельно (типовые грабли распила):
sanitize-url.ts/copy-to-clipboard.ts— побайтово идентичны оригиналам из@docmost/editor-ext(тот же@braintree/sanitize-url, та же нормализацияabout:blank→""; тот жеexecCommandCopy). Безопасность URL не деградировала."connected"-литерал ==WebSocketStatus.Connected(сверено по@hocuspocus/provider); атомyjsConnectionStatusAtomхранит сырые строки, писатель кладётevent.status— сравнение эквивалентно.{...props}(NodeViewProps) прокинуты, подключены в extensions/page-editor; старых eager-импортёров нет.Незакрытые замечания (не блокеры, на твоё усмотрение):
await import("posthog-js")(раньше posthog был eager-синхронным). Self-hosted не затронут. Не чинил намеренно: развязать это без remount'а App = риск задеть ровно ту cloud-аналитику, которую ревью подтвердило эквивалентной; вынес как возможный follow-up."connected"дублирует enum hocuspocus (документировано комментарием); LaTeX-плейсхолдер при медленном katex-чанке может кратко мигнуть сырым LaTeX.Ревью — #354 (perf(client): route + component code-splitting, #342), round 1. Вердикт: CHANGES
Отличная работа по цели #342: eager-граф РЕАЛЬНО очищен (независимо сверил import-graph walk'ом из
main.tsx— 179 eager-файлов, НОЛЬ маркеров редактор-движка@tiptap/editor-ext/@hocuspocus/prosemirror/yjsи ноль@ai-sdk/posthog/katex/excalidraw; поймали ВСЕ eager-утечки — type-only Editor, inline yjs-status, dynamic recording-import, вынос sanitize-url/copy-to-clipboard из editor-ext-барреля). Объективка зелёная: entry-чанк 552КБ (было 1920), client tsc 0, build 0, vitest 952. Безопасность чистая (sanitize-url.tsбайт-идентичен editor-ext, тот же@braintree/sanitize-url@7.1.2— XSS-защита не потеряна). Ленивые node-view'ы, latch AI-чата, native-host контракт, инлайнWebSocketStatus.Connected === "connected"— всё сверено корректным.НО сам code-splitting вводит 2 рантайм-режима отказа, которых билд-гейт не видит и которые нарушают заявленное «behavior 1:1». Критичных нет, открыто 4:
await import()ПЕРЕД единственнымrenderApp(<App/>)без try/catch → cloud-рендер блокируется на загрузке чанка, а при провале импорта (сеть / stale-404 / ад-блокер по имени чанкаposthog) cloud-юзер получает НАВСЕГДА пустую страницу.sanitize-url.tsбез теста (origin в editor-ext security-contract-тестирован).void import(...)без.catch→ unhandledrejection при провале префетча.📋 Полный отчёт (F1–F4, DROP, что сверено)
Do — почини, потом ставь
review/needsF1 [stability/architecture · high] Добавь корневой chunk-load error boundary —
apps/client/src/main.tsx(дерево рендера) + границы Suspense вApp.tsx:103,layout.tsx:250,share-layout.tsx:491.PR переводит ВСЕ роуты (Home, Page, все settings/*, share, spaces) + Aside + AiChatWindow в
React.lazy, но над Suspense-границами НЕТ ни одного error boundary (единственныеErrorBoundary— внутриpage.tsx(сам лениво-загружаемый, свой чанк-фейл не ловит), transclusion, page-embed — все глубоко в редакторе). Сценарий (классика SPA + code-splitting): юзер держит вкладку открытой → новый деплой заменяет хешированные чанки → навигация на любой роут → старый чанк-URL 404 →React.lazyреджектит → Suspense пробрасывает выше → нет boundary → всё дерево размонтируется = белый экран, без ручного релоада не восстановить. До PR все роуты были eager (навигация не тянула чанки) — это НОВЫЙ режим отказа, и роут-level он бьёт на КАЖДОЙ навигации после деплоя (взаимодействует с immutable-кэшем из #352).react-error-boundaryуже в деп'ах.Fix: оберни
<App/>вmain.tsxв error boundary, чейonError/getDerivedStateFromErrorдетектит chunk-load-фейл (ChunkLoadError//Failed to fetch dynamically imported module|error loading dynamically imported module// vitepreloadError) и делает одноразовыйwindow.location.reload()(guard от петли релоада черезsessionStorage-флаг), с обычным fallback иначе.F2 [regressions/stability · med-high] Рендери App сразу, posthog грузи асинхронно и под try/catch —
apps/client/src/main.tsx:46-73(bootstrap()).Для
isCloud() && isPostHogEnabledединственныйrenderApp(<App/>)стоит ПОСЛЕawait import("posthog-js")+await import("posthog-js/react"), без try/catch, иvoid bootstrap()без.catch. Два следствия, оба нарушают «1:1» для cloud: (а) первый рендер cloud блокируется на fetch+parse posthog-чанка (раньше posthog был в eager-бандле, рендер синхронный); (б) ХУЖЕ — если любой из этих import'ов реджектит (сетевой сбой, stale-деплой 404, или ад/privacy-блокер, матчащий чанк сposthogв имени — частый кейс, чанки называютсяposthog-js-*.jsс origin'а),bootstrap()реджектит иrenderAppНЕ достигается → cloud-юзер получает пустую страницу вместо приложения. До PR проблема с posthog максимум теряла аналитику, рендер был независим.posthog-js/reactне потребляется ни одним компонентом (grep: единственная ссылка — самmain.tsx), так что render-first безопасен.Fix: вызови
renderApp(<App/>)НЕМЕДЛЕННО, затем вtry/catchподгрузи+инициализируй posthog (при успехе — по желанию ре-рендер, обёрнутый вPostHogProvider);void bootstrap().catch(...)не должен оставлять приложение не-отрендеренным ни при каком исходе.F3 [test-coverage · medium] Добавь parity-тест перенесённого XSS-санитайзера —
apps/client/src/lib/sanitize-url.ts.Сейчас байт-идентичен editor-ext'ному
sanitizeUrl, НО origin запиннен security-contract-спеком (packages/editor-ext/src/lib/utils.spec.ts:javascript:/data:/vbscript:/JaVaScRiPt:→"",about:blank→"", безопасные https/relative/mailto выживают), а клиентская копия — без теста. Теперь две расходящиеся копии XSS-гарда: origin-тесты останутся зелёными, даже если кто-то «упростит» клиентскую копию (уронитabout:blank-нормализацию или!url-guard) → stored-XSS на рендеренных ссылках. Клиент уже гоняет vitest и имеет security-тест-сосед (lib/app-route.safe-redirect.test.ts).Fix: добавь
apps/client/src/lib/sanitize-url.test.ts, зеркалящий кейсы origin-спека.F4 [stability · low] Проглоти реджект idle-warm префетча —
apps/client/src/components/layouts/global/layout.tsx:235.void import("@/pages/page/page")оставляет реджект необработанным →unhandledrejection(шум для Sentry/глобальных хендлеров) при провале best-effort префетча (offline/stale-деплой в idle). Fix:void import("@/pages/page/page").catch(() => {}).⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору)
[below-threshold]low[conventions]@braintree/sanitize-urlвставлен между@ai-sdkи@atlaskit, ломает алфавитный порядок deps (место — перед@casl). Косметика. DROP.[superseded]low[documentation] Комментmain.tsx«cloud analytics behavior unchanged; simply deferred» недоговаривает про render-timing/blank-page стоимость — закроется вместе с F2 (правишь код — коммент за ним). DROP.[below-threshold]low[regressions] Ленивый math: первый рендер math-ноды показывает сырой LaTeX-плейсхолдер (изnode.attrs.text, верный) до загрузки katex-чанка, потом свап → однократный reflow на math-heavy shared-странице. Приемлемо, дизайн-намеренно. DROP.[below-threshold]low[coherence] Холодный прямой заход на/share/...показывает два последовательных лоадера (App full-page пока грузитсяShareLayout, потом внутренний). Косметика, не пропущенная граница. DROP.Сверено (9 аспектов + мои проверки, голова
51925e95): eager-граф чист (import-walk, 0 editor/ai-sdk/posthog/katex маркеров);WebSocketStatus.Connected === "connected"(провайдер), атом хранит сырой status → инлайн верен, tsc-clean;sanitize-url/copy-to-clipboardбайт-идентичны editor-ext; ленивые node-view'ы = проверенный excalidraw-паттерн,node.attrs.textматчит схему; AI-чат latch без race (atom персистентный, окноnullпока закрыто); AI-SDK/posthog/katex все вне eager; Suspense-границы без дыр (top-level ловит сам lazyShareLayout, per-layout держит shell смонтированным); native-hostcreatePageWithRecordingконтракт цел (dynamic import внутри уже-async, под внешним try/catch).- F1 [HIGH]: added a root ChunkLoadErrorBoundary (react-error-boundary) wrapping the routed app in main.tsx, ABOVE all the route-level/Aside/AiChatWindow Suspense boundaries. A stale-deploy chunk 404 (React.lazy reject) is caught and auto-reloads once (sessionStorage-guarded against a reload loop), else shows a manual "new version available" reload UI — instead of unmounting the whole tree to a white screen. Existing per-feature ErrorBoundaries untouched. - F2 [MED-HIGH]: posthog no longer blocks/blanks the cloud first paint. main.tsx now renders <App/> immediately for everyone, then `void initAnalytics()` — which keeps the exact cloud gate, dynamically imports posthog, and RE-RENDERS the same React root wrapped in PostHogProvider (React reconciles onto the painted DOM, so cloud ends up wrapped exactly as before). The import+init is try/catch'd: a failed analytics chunk (network / stale-404 / ad-blocker on a "posthog" chunk) degrades to no-analytics instead of a permanently blank page. - F3: sanitize-url.test.ts mirroring editor-ext's security contract (javascript:/ data:/vbscript:/obfuscated → ""; https/relative/mailto preserved). - F4: the idle-warm `void import(...)` prefetch in layout.tsx gets `.catch(()=>{})` so a failed best-effort prefetch can't surface as an unhandledrejection. No new deps (lockfile unchanged). Gate: client tsc 0, sanitize test 3/3, client build succeeds (entry chunk still 556K, posthog in separate dynamic chunks). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Починил все 4,
review/needs(коммит6475cb81). Новых зависимостей нет (лок не тронут).F1: fixed — корневой
ChunkLoadErrorBoundary(наreact-error-boundary, той же либе что page.tsx) обёрнут вокруг роут-контента вmain.tsx, ВЫШЕ всех роут-level/Aside/AiChatWindow Suspense-границ. Ловит провалReact.lazy(stale-деплой, чанк-404): авто-релоад ОДИН раз (guard по sessionStorage от петли), иначе ручной UI «доступна новая версия» с кнопкой Reload — вместо белого экрана. Пер-фичевые ErrorBoundary не тронуты.F2: fixed — posthog больше не блокирует/не гасит первый кадр на cloud.
main.tsxрендерит<App/>СРАЗУ для всех, потомvoid initAnalytics(): тот же cloud-гейт, динамический импорт posthog, и РЕ-РЕНДЕР того же React-рута, обёрнутого вPostHogProvider(React реконсайлит на уже отрисованный DOM → cloud-дерево обёрнуто ровно как раньше). Импорт+init в try/catch: провал analytics-чанка (сеть / stale-404 / адблокер по имениposthog) деградирует в «без аналитики», а не в вечно пустую страницу. Self-hosted путь — мгновенный (гейт возвращается рано).F3: fixed —
sanitize-url.test.tsзеркалит security-contract editor-ext (javascript:/data:/vbscript:/обфусцированный →""; https/относительный/mailto проходят).F4: fixed — idle-warm
void import(...)вlayout.tsxполучил.catch(()=>{})— провал best-effort префетча не всплывёт unhandledrejection'ом.📋 Объективка
tsc --noEmit— EXIT 0;sanitize-url.test— 3 passed;pnpm install --frozen-lockfile— EXIT 0 (лок не менялся).Ре-ревью — #354 (perf клиента: code-splitting, #342), round 2. Вердикт: CHANGES
F1/F3/F4 закрыты; F2 (пустой экран на cloud) починен по сути — первый кадр больше не блокируется/не гаснет на провале posthog. НО выбранный механизм фикса F2 вносит новую cloud-регрессию: пост-пейнт ре-рендер РЕМАУНТИТ всё дерево App. Открыто 2 (критичных нет).
Закрыто с round 1: F1 (корневой
ChunkLoadErrorBoundaryвыше всех роут-Suspense — ловит провал lazy-чанка, авто-релоад один раз с sessionStorage-guard, иначе ручной UI; сверил — react-error-boundary ловит reject lazy при рендере), F3 (sanitize-url.testзеркалит security-контракт editor-ext, невакуумно), F4 (idle-warm.catch).Открыто: F5 (ре-рендер posthog ремаунтит App на cloud — потеря state/фокуса/скролла + двойной запуск mount-эффектов), F6 (детектор
isChunkLoadErrorбез теста).Объективка зелёная (голова
6475cb81): frozen install 0; client tsc 0;sanitize-url.test3 passed; полный client vitest 955 passed (101 файл, +1 новый тест); entry-чанк 567К (eager-редукция #342 сохранена).📋 Do (F5, F6) + что сверено
Do — почини, потом ставь
review/needsF5 [stability/regressions/coherence · medium] Убери ре-рендер posthog — он ремаунтит App на cloud (и НИЧЕГО не даёт) —
main.tsx:60-96(initAnalytics).renderApp(<App/>)рисует сразу, затем на cloudinitAnalyticsвызываетrenderApp(<PostHogProvider><App/></PostHogProvider>)на том же руте. В слоте-ребёнкеChunkLoadErrorBoundaryтип элемента меняетсяApp→PostHogProvider→ React НЕ реконсайлит на месте, а размонтирует всё поддерево App и монтирует заново. Комментарий «reconciles the same root … wrapped as before» это скрывает — по факту ремаунт. Последствия (cloud, каждый cold-load, в окне между первым кадром и загрузкой posthog-чанка ~100-800мс): всеuseEffect([])в дереве App запускаются ДВАЖДЫ (websocket connect/disconnect,useTrackOrigin, подписки), теряется локальный state/фокус/скролл/незакоммиченный ввод (конкретно: юзер начал печатать в/loginдо загрузки аналитики → ввод и фокус слетают на ремаунте). Это регресс vs и pre-split (один монтаж), и round-1.И это ЛИШНЕЕ (сверил сам): в
apps/clientНОЛЬ потребителей PostHog-React-контекста (нетusePostHog/useFeatureFlag*/PostHogFeature— grep пуст), аPostHogProviderс переданнымclient— no-op (if (client) return,posthog-js/react/dist/esm/index.js:53,70): вся аналитика идёт через синглтонposthog.init(). Значит второйrenderAppне даёт НИЧЕГО функционально.Fix: убери второй
renderAppцеликом —initAnalytics()пусть толькоposthog.init(...)иreturn. Аналитика идентична, ремаунт/потеря-state/двойные-эффекты исчезают. Строгое упрощение.F6 [test-coverage · low] Покрой
isChunkLoadError(и по желанию reload-guard) —chunk-load-error-boundary.tsx.isChunkLoadError— чистый предикат по 4 хрупким сигналам (name==='ChunkLoadError'+ 3 regex по сообщениям Vite/браузера). Если браузер/Vite сменит имя/текст ошибки — предикат вернётfalse,onErrorno-op, авто-релоад МОЛЧА не сработает (весь смысл F1 потерян), и ни один тест не поймает. Fix: экспортируйisChunkLoadError+ таблично-тест (4 принимаемых формы + пара отклоняемых:TypeError,null); reload-guard (один релоад, второй → ручной UI) — опционально, с моком sessionStorage/location.⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору)
[below-threshold]low[stability/regressions]RELOAD_FLAGне чистится после успешной загрузки → только ОДИН авто-релоад на сессию вкладки; второй stale-деплой в той же сессии → ручной UI. Мелкая UX-деталь, строго лучше прежнего (ничего не было). Опц.: чистить флаг на успешном mount App.[below-threshold]low[conventions] Fallback хардкодит английский вместоt()— защитимо (i18n мог сам не загрузиться; есть прецедентpage-embed-view).Вне scope — пред-существующий баг (кандидат на отдельную задачу, НЕ этот PR)
[bug]main.tsx:70— гейтisCloud() && isPostHogEnabledиспользуетisPostHogEnabledкак ССЫЛКУ на функцию (не вызывает()), т.е. всегда truthy → эффективно гейт =isCloud()только, иposthog.init(getPostHogKey())может запуститься с пустым ключом. Перенесён ВЕРБАТИМ из pre-PR eager-кода (не регресс этого диффа), поэтому вне scope. Скажи — заведу issue.Сверено (9 аспектов + мои проверки, голова
6475cb81): F1-boundary выше всех lazy-границ (routes/Aside/AiChatWindow/node-views), ловит reject lazy, loop-guard корректен; F1↔F2 не конфликтуют (boundary-state не сбрасывается на ре-рендере); F3-тест невакуумен; eager-граф #342 сохранён (posthog динамический, boundary тянет только уже-eager react-error-boundary+mantine); posthog.init один раз (нет двойного $pageview); 0 потребителей posthog-контекста (F5-фикс безопасен).Починил оба,
review/needs(коммитfdb6f39a). Делал сам (мелочь).F5: fixed — убрал ре-рендер posthog целиком. Ты прав: втор
renderApp(<PostHogProvider><App/>)в слотеChunkLoadErrorBoundaryменял тип элементаApp→PostHogProvider→ React РЕМАУНТИТ всё поддерево App (mount-эффекты дважды, теряется state/фокус/ввод на cloud cold-load). И это ЛИШНЕЕ — сверил: в apps/client НОЛЬ потребителей PostHog-React-контекста,PostHogProviderс готовым client — no-op, вся аналитика через синглтонposthog.init(). ТеперьinitAnalyticsинитит ТОЛЬКО синглтон (безposthog-js/react-импорта, без второго рендера),renderApp()рисует<App/>один раз. Первый кадр мгновенный, cloud-аналитика не изменилась, ремаунта нет.F6: fixed — заэкспортил
isChunkLoadError+chunk-load-error-boundary.test.ts:ChunkLoadError-name + 3 сообщения провала динамического импорта (case-insensitive) → true; null/undefined/обычные ошибки → false. False-negative, вновь гасящий приложение на реальном чанк-404, теперь ловится.📋 Объективка
tsc --noEmit— EXIT 0;chunk-load-error-boundary.test+sanitize-url.test— 14 passed;Ре-ревью — #354 (perf клиента: code-splitting, #342), round 3. Вердикт: PASS ✅
Обе находки round 2 закрыты и сверены по коду. Вместе с F1-F4 (round 1/2) — готово к мержу.
renderApp()рисует<App/>ОДИН раз;initAnalytics()инитит только синглтонposthog.init()(без импортаposthog-js/react, без второго рендера) под тем же cloud-гейтомisCloud() && isPostHogEnabled, опции init не изменены, try/catch глотает провал. Ремаунт App на cloud исчез, первый кадр мгновенный, аналитика идентична. Коммент теперь точно объясняет почему (ноль потребителей posthog-контекста, provider с client — no-op).isChunkLoadErrorзаэкспортен +chunk-load-error-boundary.test.ts: 4 принимаемых формы (ChunkLoadError-name + 3 сообщения провала dynamic import, case-insensitive) → true; null/undefined/обычные ошибки → false. False-negative, вновь гасящий приложение на реальном чанк-404, теперь ловится тестом.Объективка зелёная (голова
fdb6f39a): frozen install 0; client tsc 0; boundary+sanitize тесты 14 passed; полный client vitest 966 passed (102 файла, +1 новый тест). eager-редукция #342 сохранена.Прим.: пред-существующий баг гейта (
isPostHogEnabledкак ссылка на функцию, main.tsx) — вне scope этого PR (перенесён вербатим из eager-кода), не трогали верно. Скажи — заведу отдельную задачу.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.