Files
houseplan-card/docs/specs/506-startup-performance.md
T
2026-09-09 12:57:12 +03:00

17 KiB
Raw Blame History

#506 — Запуск карточки без повторного layout из-за lazy summary runtime

Issue: #506. Дата: 2026-09-09. Полный трек: нарушен критерий small «нет влияния на производительность». Статус и актуальный вердикт находятся только в issue.

1. Пользователь и сценарий

J1/J2/J3 из docs/SCOPE.md: владелец, член семьи и гость быстро открывают план на desktop, телефоне или киоске, видят целостный план и текущее состояние устройств. Администратор также переключает View/редакторы без скачков камеры.

До: повторное открытие карточки иногда создаёт ненужные тяжёлые перерисовки сразу после первого кадра; обновление света попадает на повторное согласование размеров. После: уже загруженный код сводной панели доступен новому экземпляру до первого измерения шапки. Функции, настройки и внешний вид не меняются.

2. Доказательства и границы вывода

Full Performance 34335024892 сравнил fff171c7dc3994bd884d95b812bd82b16b69f080 (продукт v1.72.0) и d11f95bc40ea5ba6749ea3290b241b30f02998a6 (неопубликованный stable-кандидат). На каждом профиле база и кандидат запускались на одном Linux-runner: 7 образцов после 1 прогрева.

Профиль / метрика База Кандидат Допустимый предел
blend, stateUpdate1 median ms 49.9 130.4 99.9
blend, longTask.countP95 2 5 4
overlay, stateUpdate1 median ms 217.3 808.8 325.95
overlay, longTask.totalP95 ms 1142 1735 1713
isometric, longTask.countP95 23 29 27.6

Независимая неизменённая Linux-пара blend повторила 50.3 → 146.1 ms при пределе 100.3 ms. Во всех семи образцах первый HA tick у базы вызывает один performUpdate, у кандидата — четыре. Внешняя диагностическая трасса:

  1. Новый экземпляр принимает warm snapshot и сохранённую высоту шапки.
  2. _summary ещё отсутствует: даже прогретый модуль подключается через асинхронный import().then() в connectedCallback.
  3. Первый render/updated измеряет шапку без summary controls.
  4. Подключение summary добавляет controls; stage меняется 782 → 724 px.
  5. Отложенный _refitView создаёт continuity-кандидат stage-resize и ещё три render-прохода holding/candidate/commit. Проверки _booting=false и continuity=steady в начале tick не исключают уже ожидающий refit.

Glow benchmark по контракту исключает mount cost, но исправляется именно ненужный layout продукта, а не окно замера. Изометрический benchmark намеренно включает load: уникальный title каждого sample даёт новый config/warm key. Его JS-модуль прогрет, но card boot холодный. Load long tasks выросли с 3 до 6–9; повторный structural rebuild не обнаружен. Общая причина правдоподобна, но весь прирост isometric пока НЕ доказан как summary-owned.

3. Объём задачи

Включены:

  • загрузчик/фабрика summary runtime и её подключение к lifecycle карточки;
  • устранение позднего header roundtrip у экземпляров с уже загруженным модулем;
  • доказательства независимости карточек и корректной отмены отложенного подключения;
  • узкие unit/browser/mutation проверки и неизменённые performance-пары.

Не включены: общий рефакторинг #500, новые функции панели, изменение её дизайна, опций, доступности или прав; backend/config миграции; изменение геометрии света; переписывание continuity/camera; удаление из замеров load, увеличение budgets, сокращение samples или arbitrary sleeps. Другой источник регресса, не связанный с этой цепочкой, требует отдельного явно согласованного расширения/issue.

4. Контракт загрузки и владения

4.1. Код отдельно от состояния

Допустим кеш успешно загруженного конструктора или фабрики кода в пределах страницы/модуля. Новый host всегда получает новый runtime. Нельзя кешировать между карточками host, controller/runtime instance, HA данные, user/card identity, настройки, drafts, DOM, подписки, таймеры, observers, metrics или viewport.

Если фабрика уже доступна, runtime нового host создаётся синхронно при подключении после super.connectedCallback(), до первого Lit render/измерения header. Восстановление warm frame и cold card boot с прогретым модулем получают одинаково полный состав summary controls. Существующий instance при same-node reconnect переиспользуется, а не создаётся заново.

Первый cold load сохраняет динамическую границу: loaded runtime и его зависимости не переносятся eager в entry bundle. Одновременные cold mounts могут разделять один pending import, но не instance. Каждый host подключает только свой runtime.

4.2. Асинхронный lifecycle

Pending import после disconnect не должен подключить runtime к отключённому host, вызвать новые UI side effects или восстановить старый host после его замены. Повторный connect во время pending import не создаёт duplicate runtime/connect. Завершение старой попытки не перезаписывает уже созданный instance и не запускает второй connect поверх актуального lifecycle.

Успешный импорт можно сохранить как фабрику независимо от того, остался ли его инициатор подключён. Ошибка загрузки не должна навсегда кешировать отказ: следующая штатная попытка подключения может повторить импорт. Автоматический бесконечный retry, новые сообщения пользователю и обязательный page reload не нужны. Сохранить существующее безопасное поведение карточки при невозможности загрузить опциональный runtime. Инвалидирование snapshot/subscription membership при первом создании runtime должно сохраниться: новые summary entities не теряют live updates.

4.3. Layout и визуальная непрерывность

При неизменных host/chrome/config и прогретой фабрике первый показанный кадр имеет полный header; его высота и stage совпадают с окончательными размерами. Не должно быть второго summary-driven header measurement/refit/continuity-кандидата после готовности плана. На детерминированном glow fixture первый неизменяющий геометрию HA tick снова даёт один performUpdate, без наложенного stage-resize каскада.

Настоящий resize, поздний HA chrome, изменение ширины/высоты контейнера, видимости панели и переход редактора не подавляются. Сохраняются viewport/zoom, warm identity, обработка zero-size и hidden tab, current-token checks, complete-frame handoff, paint barrier и существующий режим восстановления подключения. _refitView и continuity state machine не изменяются для обхода этого регресса.

5. UX, данные, совместимость

Новых кнопок, строк, настроек и пользовательских решений нет; i18n не затрагивается. Сводная панель остаётся lazy; её prefs и drafts принадлежат конкретному runtime. Внешний вид, анимации, overlay layout, accessibility и desktop-first редакторы соответствуют UX-MODES.md, TOUCH-SUPPORT.md и действующему #505. Модель хранения, config version и сетевые API неизменны, миграция не нужна. Смена пользователя, placement/card identity или маршрута не получает состояние другого экземпляра через фабрику. Холодная страница и загрузка после ошибки остаются работоспособными. Работа не должна увеличить eager bundle сверх текущего бюджета.

6. Критерии приёмки и доказательства

AC Ожидание Доказательство
AC1 Прогретая фабрика синхронно создаёт отдельный runtime для каждого нового host; same-node reconnect сохраняет свой instance Unit loader/factory: два host, distinct instances, повторное подключение; browser assertion до первого render
AC2 Cold concurrent mounts не дублируют импорт; ошибка допускает retry; pending disconnect/reconnect не даёт duplicate connect или stale side effects Управляемые deferred promise unit tests success/failure/disconnect/reconnect, плюс browser lifecycle
AC3 Нет cross-card/user preferences, drafts, DOM/подписок; подключение не теряет summary entity invalidation Существующие summary runtime identity/live-state tests + новые assertions владения; разные host/card/user cases
AC4 При width 780 px и длинном title warm replacement не даёт позднего summary/header roundtrip: первый полный header и stage стабильны, нет stage-resize кандидата при неизменной геометрии Детерминированный browser smoke: покадровые размеры, runtime presence, refit/continuity/render counters; новый cold-key экземпляр с прогретым модулем тоже проверяется
AC5 Первый HA brightness tick без изменения геометрии не имеет startup resize каскада Browser smoke на измеряемом glow fixture, ожидается один performUpdate и отсутствие stage-resize; исходная paired benchmark trace
AC6 Реальные resize/поздний chrome, View/все редакторы, скрытая/показанная панель, kiosk/dashboard/panel host и zero-size сохраняют поведение без пустого кадра Warm-remount/preloader/continuity/summary/mode browser suite; хотя бы один положительный resize witness в новом smoke; CDP screencast
AC7 Исходные blend, overlay и isometric проходят все текущие paired performance checks Linux 7 samples + 1 warmup, неизменные scripts/windows/budgets, база fff171c7 или эквивалентный продукт v1.72.0; isometric load включён полностью
AC8 Итоговый релиз имеет полные неизменённые gates, а узкий тест действительно ловит дефект Negative witness: отключение synchronous cached path проваливает новый smoke; зарегистрированная mutation; Full Validate + все 9 Full Performance profiles на итоговом SHA

Список файлов тестов — ориентир, а не требование менять каждый из них: summary-panel-runtime.test, boot-soft-layout.test, visual-continuity.test, smoke_warm_remount, smoke_preloader_lifecycle, smoke_visual_continuity, summary/mode/viewport smoke и smoke_isometric_contract. Защитный browser AC4/AC5 включается в scripts/mutation-gate.mjs с фактическим negative witness; source-text assertion без исполнения недостаточен.

7. Техническая гипотеза, риски и откат

Предпочтительный дизайн: небольшой typed loader с factory cache + pending import, per-host lifecycle guard и синхронный fast path в connectedCallback. Точные имена и границы модуля допускается уточнить в реализации при сохранении контракта. Static import loaded runtime вместо lazy factory недопустим.

Риски: двойное подключение при overlapping promises; кеширование failure; утечка host/state; поздняя invalidation summary entities; незаметный перенос runtime в eager chunk; исправление только glow без устранения isometric regression. Первые риски закрываются AC1–AC6 и bundle checks. Последний — обязательным AC7; если гипотеза не закрывает isometric, stable остаётся заблокирован, результаты фиксируются в issue, бюджеты не ослабляются. Ретраи не заменяют объяснение причины.

Откат: revert scoped commit, данные совместимы в обе стороны. При возврате регресса stable не публиковать; не откатывать публичные теги и не очищать prefs.

8. Документация и release artifacts

  • Оба changelog: docs/CHANGELOG.md, docs/CHANGELOG.ru.md, ссылка #506; пользовательское исправление — более стабильное/быстрое открытие плана.
  • docs/ARCHITECTURE.md: код фабрики кешируется на страницу, runtime/state — на экземпляр, cold lazy boundary и lifecycle guard. При необходимости уточнить docs/WARM-REMOUNT.md; пользовательское руководство не меняется (UX прежний).
  • Новые визуальные эталоны дизайна не требуются: ожидаем тот же итоговый вид. Если src меняет docs fingerprint, перед релизом переснять полный набор в Linux, выполнить визуальную приёмку manifest/screenshots по действующему docs-процессу, не принимать автоматически и не исключать свидетелей без отдельного основания.
  • Приложить unit/typecheck/build/smoke/mutation evidence, before/after paired JSON, Linux Full Performance и CDP continuity artifact с exact SHA/окружением.
  • Версии/собранные полные bundle trees обновляются штатным release-процессом. После S8 выпускается новая бета, затем stable только после всех gates. Ни наличие предыдущего failed candidate в main, ни зелёный beta smoke не заменяет сравнение итогового stable с продуктом v1.72.0 и полную проверку всех 9 профилей.