perf(editor): срезать работу на каждый keystroke — латентность печати (#343) #357
Open
agent_coder
wants to merge 1 commits from
perf/343-typing-latency into develop
pull from: perf/343-typing-latency
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:perf/342-code-splitting
vvzvlad:feat/355-perf-metrics
vvzvlad:perf/346-compression-cache
vvzvlad:feat/git-sync-2
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#357
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/343-typing-latency"
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 #343.
ProseMirror сам настроен верно — тормозила обвязка: на каждую транзакцию (и на каждый ЧУЖОЙ keystroke при коллаборации) гонялись десятки селекторов меню + полная сериализация документа. Поведение всех меню и фич 1:1 — меняется только КОГДА выполняется работа.
getJSON()вне горячего пути:onUpdateбольше не сериализует весь документ синхронно — сериализация ушла внутрь 3-секундного дебаунса (новый хукuse-page-content-cache.ts), с flush при размонтировании (последний снапшот не теряется).docChangedслиты в ОДИН; при отсутствии сносок полный обход пропускается (детекция вставки по step-slice — покрывает ввод/paste/коллаборацию).editor.can().undo()/.redo()(dry-run на каждый keystroke) → дешёвое чтение глубины истории (Yjs undoManager stack / pm-history depth).remote.attach()вынесен из тела рендера вuseEffect.isActiveвместоgetAttributes+ проб выравнивания, пока их тип не активен (shouldShowпо-прежнему рулит показом — появляются ровно как раньше).selectionUpdate-listener теперь вешается только для mermaid (единственный потребитель selected-состояния) — для обычных блоков ноль listener'ов/setState на движение каретки.How verified
editor-extbuild + 252/252 тестов; клиентские editor-тесты — pass;tsc --noEmit— 0; client build — ок. Новых зависимостей нет.Deferred (осознанно, риск коллаборативного hot-path — вынесено в тексте issue как «отложить, если небезопасно»)
Checklist
Незакрытые SUGGESTION (не блокеры): module-global memo в slash-command (общий на инстансы, но ключ — query, безвреден); TOC-debounce без flushOnUnmount (осознанно для производного UI).
Ревью — #357 (perf(editor): срезать работу на keystroke, #343), round 1. Вердикт: PASS ✅
Механизм корректен, «behavior 1:1» подтверждается: горячий путь разгружен без изменения поведения. Критичных/блокирующих проблем нет — готово к мержу.
Объективка зелёная (мой полный прогон, голова
e9e4c102, CI-условия, чистое дерево): frozen install 0; editor-ext build 0; editor-ext vitest 252 passed (вкл. новыйfootnote-numbering.test— 6 кейсов: zero-traversal short-circuit со шпионом наdescendants, реактивация при первой вставке, порядок нумерации, паритет декораций, renumber); client tsc 0; client vitest 955 passed (вкл. новыйuse-page-content-cache.test— 3 кейса: getJSON вне sync-пути, flush на unmount, skip при destroyed).📋 Что сверено по каждому аспекту (методология — см. прим. внизу)
Сверено (риск-поверхности перепроверены по коду напрямую):
[stability/regressions · КРУЦ] undo/redo depth-read = ТОЧНО эквивалентен
can()—use-toolbar-state.ts:97-136. НовыйhistoryAvailabilityчитаетyUndoPluginKey.getState(state).undoManager.undoStack.length > 0/redoStack.length > 0для коллаба,undoDepth/redoDepth > 0для plain. Сверил в исходнике@tiptap/y-tiptap(dist/y-tiptap.js:2104-2127): САМА undo-команда гейтится ровноundoManager.undoStack.length > 0/redoStack.length > 0— байт-в-байт то же. КлючyUndoPluginKeyсуществует и экспонируетundoManager.undoStack/redoStack(:2067). Риск «коллаб-undo молча выключен» — ОПРОВЕРГНУТ. Pre-sync static editor (нет ни одного backend'а) → обе ветки → 0 → false, как старыйsafeCan.[regressions] Гейтинг меню на
isActive— паритет сshouldShow— audio/image/videoshouldShow=isActive(type) && getAttributes(type).src; pdf/callout =isActive(type). Селектор теперь возвращает null/false при!isActive— это ПОДмножествоshouldShow=false, т.е. значение никогда не рендерится.calloutхардкодisCallout:trueверен (входит только при активном callout).subpagesпри неактивном →false= дефолтgetAttributes.recursive ?? false. Поведение сохранено.[coherence] Footnote single-walk + short-circuit —
EMPTY_STATE-синглтон безопасен: потребители (footnote-reference-view/definition-view) только ЧИТАЮТ черезgetFootnoteNumber(нет мутации map/decorations).computeFootnoteNumbers/RefCountsне сломаны — живут вfootnote-util.ts, используютсяfootnote-canonicalize.ts; изfootnote-numbering.tsубрано только ИХ использование (инлайн в один обход).transactionInsertsFootnoteсканируетstep.slice— footnote-ноды входят только через Replace/ReplaceAround (у обоих есть.slice); mark/attr-степы ноды не вводят; remote-коллаб-степы тоже Replace → детекция полна.hasFootnotesкорректно возвращается в false после удаления всех (пока true — всегда rebuild → пересчёт).[stability] getJSON-кэш — flicker-only, не потеря данных —
['pages', slugId]это локальный react-query flicker-кэш; источник истины — Yjs/collab (независимый sync). Дебаунс 3с +flushOnUnmount:true+ guardisDestroyed→ максимум ≤3с устаревания локального кэша, не персистентности. Хук объявлен ДОuseEditor(cleanup-порядок), flush читает живой editor.[security] LGTM (независимый субагент отработал): slugId-скоуп кэша не изменён; slash-memo кэширует статический каталог команд по query (не контент); footnote
style-строка из монотонного счётчика (не user-input), sink байт-идентичен старому.[stability] code-block —
isSelectedчитается ТОЛЬКО на строке mermaid-видимости (language === "mermaid" && !isSelected); гейтинг listener'а на mermaid корректен,updateSelection()на attach +language-dep инициализирует при переключении в mermaid.[stability]
remote.attach()в effect — вынесен из тела рендера (был side-effect-in-render), идемпотентен, deps[providersReady, pageId]ловят пере-создание провайдера при свопе.[stability] slash-command module-memo —
getSuggestionItemsчистая от query; module-levellastQuery/lastResultинвалидируется при смене query, между инстансами редактора безопасно (результат — функция только query).⛔ DROP — кодеру НЕ делать · калибровочный лог (оператору)
[below-threshold]low[test-coverage]historyAvailability(undo/redo depth, вкл. коллаб-веткуyUndoPluginKey) без юнит-теста — в отличие от footnote/getJSON, которые тесты получили. Реально корректен (сверил эмпирически с исходником команды), но связан с ВНУТРЕННЕЙ формой@tiptap/y-tiptap(undoManager.undoStack) → при апгрейде либы может тихо сломаться (undo-кнопка всегда disabled), тест бы это ловил. Дешёвый drift-guard, но не дефект этой правки (поведение верно) → below-threshold. Кандидат, если мейнтейнер хочет застраховать связь с приват-API.[below-threshold]low[architecture]use-toolbar-state.tsлезет в приватный plugin-state@tiptap/y-tiptap. Осознанный перф-компромисс (тот же shape, что читает сама команда), задокументирован. Автор вправе оставить.Прим. по методологии этого раунда: параллельный веер субагентов упёрся в недельный лимит (сбросится в 00:00 МСК) — 8 из 9 аспектов не отработали, security отработал (LGTM). Дифф маленький (15 файлов, ~470 строк, я прочитал целиком), поэтому мульти-аспектный анализ выполнил напрямую как оркестратор, перепроверив КАЖДУЮ риск-поверхность по коду (список выше — не рассуждения, а сверка с исходником либ и тестами). Объективка прогнана реально. Если нужен независимый веер «свежими глазами» — при следующем
review/needsон переотработает (лимит к тому времени сброшен); скажи, если хочешь дождаться его перед мержем.View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.