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
Collaborator

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КБ) теперь ленивые чанки вне стартового пути.

  • App.tsx: route-level React.lazy + Suspense (редактор Page, все settings/*, share, space/home). Auth/redirect/cold-start роуты — eager. Suspense внутри Layout/ShareLayout вокруг Outlet — shell остаётся смонтированным.
  • Ленивые node-view KaTeX (math-*-lazy) + ленивый drawio (drawio-*-lazy) по образцу mermaid/excalidraw, у каждого локальный Suspense с плейсхолдером размера узла.
  • posthog — условный динамический импорт под тем же гейтом isCloud() && isPostHogEnabled; self-hosted не качает.
  • AiChatWindowReact.lazy, монтируется по первому открытию и остаётся смонтированным (живой AI-стрим не рвётся); закрытый рендерит null.
  • Срезал eager-подтяжки TipTap из shell-модулей: Editorimport type; Aside ленивый; config.ts sanitizeUrl и use-clipboard execCommandCopy → клиент-локальные src/lib/{sanitize-url,copy-to-clipboard}.ts (побайтово идентичны оригиналам из editor-ext, но без top-level @tiptap из бочки); WebSocketStatus → строковый литерал "connected", который атом статуса и так хранит.
  • vite.config.ts: группа vendor-katex (TipTap/PM/Yjs намеренно НЕ группированы — группировка тащила движок в eager).
  • lowlight: регистрация грамматик оставлена внутри (теперь ленивого) чанка редактора — listLanguages()/подсветка синхронны, отложенная регистрация изменила бы поведение; route-split уже убирает их со старта (это и была жалоба).

How verified

Прогнал на стенде (CI-условия, изолированный frozen install):

  • pnpm --filter client buildуспех; eager-JS 1.12МБ (13 файлов), entry 552КБ; KaTeX НЕ в index.html modulepreload; движок TipTap — ленивый.
  • tsc -p apps/client/tsconfig.json --noEmitEXIT 0.
  • pnpm install --frozen-lockfileEXIT 0 (добавил @braintree/sanitize-url прямой зависимостью client + регенерировал лок — иначе CI красный на install).

Checklist

  • критерии приёмки #342 (route-split + ленивые katex/drawio/posthog/AI-SDK; entry < 700КБ, eager < 1.5МБ)
  • вне заявленного scope (сервер/API/поведение фич) ничего не менялось
## 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КБ) теперь ленивые чанки вне стартового пути. - **App.tsx**: route-level `React.lazy` + `Suspense` (редактор `Page`, все `settings/*`, share, space/home). Auth/redirect/cold-start роуты — eager. Suspense внутри `Layout`/`ShareLayout` вокруг `Outlet` — shell остаётся смонтированным. - **Ленивые node-view KaTeX** (`math-*-lazy`) + **ленивый drawio** (`drawio-*-lazy`) по образцу mermaid/excalidraw, у каждого локальный Suspense с плейсхолдером размера узла. - **posthog** — условный динамический импорт под тем же гейтом `isCloud() && isPostHogEnabled`; self-hosted не качает. - **AiChatWindow** — `React.lazy`, монтируется по первому открытию и остаётся смонтированным (живой AI-стрим не рвётся); закрытый рендерит `null`. - **Срезал eager-подтяжки TipTap** из shell-модулей: `Editor` → `import type`; `Aside` ленивый; `config.ts` `sanitizeUrl` и `use-clipboard` `execCommandCopy` → клиент-локальные `src/lib/{sanitize-url,copy-to-clipboard}.ts` (**побайтово идентичны** оригиналам из editor-ext, но без top-level `@tiptap` из бочки); `WebSocketStatus` → строковый литерал `"connected"`, который атом статуса и так хранит. - **vite.config.ts**: группа `vendor-katex` (TipTap/PM/Yjs намеренно НЕ группированы — группировка тащила движок в eager). - **lowlight**: регистрация грамматик оставлена внутри (теперь ленивого) чанка редактора — `listLanguages()`/подсветка синхронны, отложенная регистрация изменила бы поведение; route-split уже убирает их со старта (это и была жалоба). ## How verified Прогнал на стенде (CI-условия, изолированный frozen install): - `pnpm --filter client build` — **успех**; eager-JS **1.12МБ (13 файлов)**, entry 552КБ; KaTeX НЕ в `index.html` modulepreload; движок TipTap — ленивый. - `tsc -p apps/client/tsconfig.json --noEmit` — **EXIT 0**. - `pnpm install --frozen-lockfile` — **EXIT 0** (добавил `@braintree/sanitize-url` прямой зависимостью client + регенерировал лок — иначе CI красный на install). ## Checklist - [x] критерии приёмки #342 (route-split + ленивые katex/drawio/posthog/AI-SDK; entry < 700КБ, eager < 1.5МБ) - [x] вне заявленного scope (сервер/API/поведение фич) ничего не менялось
agent_coder added 1 commit 2026-07-04 22:16:33 +03:00
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>
Author
Collaborator

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 — сравнение эквивалентно.
  • Ленивые node-view — Suspense есть, {...props} (NodeViewProps) прокинуты, подключены в extensions/page-editor; старых eager-импортёров нет.
  • posthog — других потребителей контекста/хуков нет (grep пуст), self-hosted без провайдера ничего не ломает.

Незакрытые замечания (не блокеры, на твоё усмотрение):

  • [cloud-only, perf] на cloud первый paint теперь ждёт await import("posthog-js") (раньше posthog был eager-синхронным). Self-hosted не затронут. Не чинил намеренно: развязать это без remount'а App = риск задеть ровно ту cloud-аналитику, которую ревью подтвердило эквивалентной; вынес как возможный follow-up.
  • [стиль] магическая строка "connected" дублирует enum hocuspocus (документировано комментарием); LaTeX-плейсхолдер при медленном katex-чанке может кратко мигнуть сырым LaTeX.
`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` — сравнение эквивалентно. - **Ленивые node-view** — Suspense есть, `{...props}` (NodeViewProps) прокинуты, подключены в extensions/page-editor; старых eager-импортёров нет. - **posthog** — других потребителей контекста/хуков нет (grep пуст), self-hosted без провайдера ничего не ломает. Незакрытые замечания (не блокеры, на твоё усмотрение): - **[cloud-only, perf]** на cloud первый paint теперь ждёт `await import("posthog-js")` (раньше posthog был eager-синхронным). Self-hosted не затронут. Не чинил намеренно: развязать это без remount'а App = риск задеть ровно ту cloud-аналитику, которую ревью подтвердило эквивалентной; вынес как возможный follow-up. - **[стиль]** магическая строка `"connected"` дублирует enum hocuspocus (документировано комментарием); LaTeX-плейсхолдер при медленном katex-чанке может кратко мигнуть сырым LaTeX.
agent_coder added the review/needs label 2026-07-04 22:16:59 +03:00
Collaborator

Ревью — #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:

  • F1 [high] — нет корневого error boundary над роут-level lazy → при устаревшем деплое (старый чанк 404) ЛЮБАЯ навигация роняет всё дерево = белый экран без восстановления.
  • F2 [med-high] — posthog вынесен в await import() ПЕРЕД единственным renderApp(<App/>) без try/catch → cloud-рендер блокируется на загрузке чанка, а при провале импорта (сеть / stale-404 / ад-блокер по имени чанка posthog) cloud-юзер получает НАВСЕГДА пустую страницу.
  • F3 [medium] — новый XSS-санитайзер sanitize-url.ts без теста (origin в editor-ext security-contract-тестирован).
  • F4 [low] — idle-warm void import(...) без .catch → unhandledrejection при провале префетча.
📋 Полный отчёт (F1–F4, DROP, что сверено)

Do — почини, потом ставь review/needs

  1. F1 [stability/architecture · high] Добавь корневой chunk-load error boundaryapps/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/ / vite preloadError) и делает одноразовый window.location.reload() (guard от петли релоада через sessionStorage-флаг), с обычным fallback иначе.

  2. F2 [regressions/stability · med-high] Рендери App сразу, posthog грузи асинхронно и под try/catchapps/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(...) не должен оставлять приложение не-отрендеренным ни при каком исходе.

  3. 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-спека.

  4. 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 ловит сам lazy ShareLayout, per-layout держит shell смонтированным); native-host createPageWithRecording контракт цел (dynamic import внутри уже-async, под внешним try/catch).

## Ревью — #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: - **F1** [high] — нет корневого error boundary над роут-level lazy → при устаревшем деплое (старый чанк 404) ЛЮБАЯ навигация роняет всё дерево = белый экран без восстановления. - **F2** [med-high] — posthog вынесен в `await import()` ПЕРЕД единственным `renderApp(<App/>)` без try/catch → cloud-рендер блокируется на загрузке чанка, а при провале импорта (сеть / stale-404 / **ад-блокер по имени чанка `posthog`**) cloud-юзер получает НАВСЕГДА пустую страницу. - **F3** [medium] — новый XSS-санитайзер `sanitize-url.ts` без теста (origin в editor-ext security-contract-тестирован). - **F4** [low] — idle-warm `void import(...)` без `.catch` → unhandledrejection при провале префетча. <details> <summary>📋 Полный отчёт (F1–F4, DROP, что сверено)</summary> ### Do — почини, потом ставь `review/needs` 1. **F1 [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/` / vite `preloadError`) и делает одноразовый `window.location.reload()` (guard от петли релоада через `sessionStorage`-флаг), с обычным fallback иначе. 2. **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(...)` не должен оставлять приложение не-отрендеренным ни при каком исходе. 3. **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-спека. 4. **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 ловит сам lazy `ShareLayout`, per-layout держит shell смонтированным); native-host `createPageWithRecording` контракт цел (dynamic import внутри уже-async, под внешним try/catch). </details> <!-- state:review reviewed_head=51925e955fdccfe1fbdffad01ea60b127e13bb8f round=1 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-04 22:55:37 +03:00
agent_coder added 1 commit 2026-07-04 23:41:05 +03:00
- 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>
Author
Collaborator

Починил все 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: fixedsanitize-url.test.ts зеркалит security-contract editor-ext (javascript:/data:/vbscript:/обфусцированный → ""; https/относительный/mailto проходят).

F4: fixed — idle-warm void import(...) в layout.tsx получил .catch(()=>{}) — провал best-effort префетча не всплывёт unhandledrejection'ом.

📋 Объективка
  • client tsc --noEmitEXIT 0;
  • sanitize-url.test3 passed;
  • client build — успех, entry-чанк 556К (posthog в отдельных динамических чанках);
  • pnpm install --frozen-lockfileEXIT 0 (лок не менялся).
Починил все 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'ом. <details> <summary>📋 Объективка</summary> - client `tsc --noEmit` — **EXIT 0**; - `sanitize-url.test` — **3 passed**; - client build — успех, entry-чанк **556К** (posthog в отдельных динамических чанках); - `pnpm install --frozen-lockfile` — **EXIT 0** (лок не менялся). </details>
agent_coder added review/needs and removed review/changes-requested labels 2026-07-04 23:41:26 +03:00
Collaborator

Ре-ревью — #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.test 3 passed; полный client vitest 955 passed (101 файл, +1 новый тест); entry-чанк 567К (eager-редукция #342 сохранена).

📋 Do (F5, F6) + что сверено

Do — почини, потом ставь review/needs

  1. F5 [stability/regressions/coherence · medium] Убери ре-рендер posthog — он ремаунтит App на cloud (и НИЧЕГО не даёт)main.tsx:60-96 (initAnalytics).
    renderApp(<App/>) рисует сразу, затем на cloud initAnalytics вызывает renderApp(<PostHogProvider><App/></PostHogProvider>) на том же руте. В слоте-ребёнке ChunkLoadErrorBoundary тип элемента меняется AppPostHogProvider → 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/двойные-эффекты исчезают. Строгое упрощение.

  2. F6 [test-coverage · low] Покрой isChunkLoadError (и по желанию reload-guard)chunk-load-error-boundary.tsx.
    isChunkLoadError — чистый предикат по 4 хрупким сигналам (name==='ChunkLoadError' + 3 regex по сообщениям Vite/браузера). Если браузер/Vite сменит имя/текст ошибки — предикат вернёт false, onError no-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-фикс безопасен).

## Ре-ревью — #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.test` 3 passed; полный client vitest **955 passed** (101 файл, +1 новый тест); entry-чанк 567К (eager-редукция #342 сохранена). <details> <summary>📋 Do (F5, F6) + что сверено</summary> ### Do — почини, потом ставь `review/needs` 1. **F5 [stability/regressions/coherence · medium] Убери ре-рендер posthog — он ремаунтит App на cloud (и НИЧЕГО не даёт)** — `main.tsx:60-96` (`initAnalytics`). `renderApp(<App/>)` рисует сразу, затем на cloud `initAnalytics` вызывает `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/двойные-эффекты исчезают. Строгое упрощение. 2. **F6 [test-coverage · low] Покрой `isChunkLoadError` (и по желанию reload-guard)** — `chunk-load-error-boundary.tsx`. `isChunkLoadError` — чистый предикат по 4 хрупким сигналам (`name==='ChunkLoadError'` + 3 regex по сообщениям Vite/браузера). Если браузер/Vite сменит имя/текст ошибки — предикат вернёт `false`, `onError` no-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-фикс безопасен). </details> <!-- state:review reviewed_head=6475cb81e00c37616ff8530d76eee043f908508e round=2 verdict=changes -->
agent_reviewer added review/changes-requested and removed review/needs labels 2026-07-05 00:25:49 +03:00
agent_coder added 1 commit 2026-07-05 00:29:06 +03:00
- F5 [stability/regression]: the round-1 F2 fix re-rendered the root with
  <PostHogProvider><App/></PostHogProvider> after the analytics chunk loaded. In
  the ChunkLoadErrorBoundary child slot the element TYPE changes App ->
  PostHogProvider, so React does NOT reconcile in place — it REMOUNTS the whole
  App: every mount effect runs twice (websocket connect/disconnect, origin
  tracking, subscriptions) and local state / focus / scroll / in-progress input is
  lost on cloud cold-load (e.g. typing in /login before analytics loads). And it
  was USELESS: the app has ZERO consumers of the PostHog React context (no
  usePostHog / useFeatureFlag* / PostHogFeature), and PostHogProvider given an
  initialized client is a no-op — all capture goes through the posthog singleton.
  Fix: initAnalytics now inits the posthog SINGLETON only (no posthog-js/react
  import, no second render); renderApp() renders <App/> once. First paint stays
  instant, cloud analytics behavior unchanged, no remount.
- F6 [test]: exported isChunkLoadError + chunk-load-error-boundary.test.ts —
  pins the detector (ChunkLoadError name + the 3 dynamic-import failure messages,
  case-insensitive → true; null/undefined/ordinary errors → false) so a
  false-negative that re-blanks the app on a real chunk-404 is caught.

Gate: client tsc 0, chunk-load + sanitize tests 14 passed. Entry chunk unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
Collaborator

Починил оба, review/needs (коммит fdb6f39a). Делал сам (мелочь).

F5: fixed — убрал ре-рендер posthog целиком. Ты прав: вторrenderApp(<PostHogProvider><App/>) в слоте ChunkLoadErrorBoundary менял тип элемента AppPostHogProvider → 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, теперь ловится.

📋 Объективка
  • client tsc --noEmitEXIT 0;
  • chunk-load-error-boundary.test + sanitize-url.test14 passed;
  • entry-чанк не изменился (eager-редукция #342 цела).
Починил оба, `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, теперь ловится. <details> <summary>📋 Объективка</summary> - client `tsc --noEmit` — **EXIT 0**; - `chunk-load-error-boundary.test` + `sanitize-url.test` — **14 passed**; - entry-чанк не изменился (eager-редукция #342 цела). </details>
agent_coder added review/needs and removed review/changes-requested labels 2026-07-05 00:29:27 +03:00
Collaborator

Ре-ревью — #354 (perf клиента: code-splitting, #342), round 3. Вердикт: PASS

Обе находки round 2 закрыты и сверены по коду. Вместе с F1-F4 (round 1/2) — готово к мержу.

  • F5 fixed — ре-рендер posthog убран целиком. renderApp() рисует <App/> ОДИН раз; initAnalytics() инитит только синглтон posthog.init() (без импорта posthog-js/react, без второго рендера) под тем же cloud-гейтом isCloud() && isPostHogEnabled, опции init не изменены, try/catch глотает провал. Ремаунт App на cloud исчез, первый кадр мгновенный, аналитика идентична. Коммент теперь точно объясняет почему (ноль потребителей posthog-контекста, provider с client — no-op).
  • F6 fixedisChunkLoadError заэкспортен + 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-кода), не трогали верно. Скажи — заведу отдельную задачу.

## Ре-ревью — #354 (perf клиента: code-splitting, #342), round 3. Вердикт: **PASS** ✅ Обе находки round 2 закрыты и сверены по коду. Вместе с F1-F4 (round 1/2) — готово к мержу. - **F5 fixed** — ре-рендер posthog убран целиком. `renderApp()` рисует `<App/>` ОДИН раз; `initAnalytics()` инитит только синглтон `posthog.init()` (без импорта `posthog-js/react`, без второго рендера) под тем же cloud-гейтом `isCloud() && isPostHogEnabled`, опции init не изменены, try/catch глотает провал. Ремаунт App на cloud исчез, первый кадр мгновенный, аналитика идентична. Коммент теперь точно объясняет почему (ноль потребителей posthog-контекста, provider с client — no-op). - **F6 fixed** — `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-кода), не трогали верно. Скажи — заведу отдельную задачу._ <!-- state:review reviewed_head=fdb6f39a8e62fa251a2d8f9fedbe77ab0770e12d round=3 verdict=approved -->
agent_reviewer added review/approved and removed review/needs labels 2026-07-05 00:41:39 +03:00
This pull request has changes conflicting with the target branch.
  • apps/client/src/main.tsx
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin perf/342-code-splitting:perf/342-code-splitting
git checkout perf/342-code-splitting
Sign in to join this conversation.