Files
progcode/editorial/agent-rewrites/216.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
16 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 216,
"slug": "editorial-2022-01-practice-ssr-csr",
"title": "SSR и CSR без рассинхронизации: как принять snapshot и не запустить второй запрос",
"excerpt": "Сервер уже показал данные, но клиент снова их загружает и меняет первый экран. Разбираем владельцев состояния, версию snapshot, проверку hydrate и явный путь для mismatch.",
"contentHtml": "<p>Симптом появляется сразу после загрузки страницы: сервер уже отдал карточку с данными, а после запуска JavaScript браузер повторяет тот же запрос. Иногда ответы совпадают, и ошибка остаётся незаметной. При задержке или изменении записи пользователь видит один текст, затем другой. В DevTools появляются два запроса, а команда не может объяснить, какой из них владел первым экраном. Цена ошибки — лишний сетевой путь, более длинная критическая цепочка и риск тихого рассинхрона между HTML и клиентским состоянием.</p>\n<p>Тезис простой: SSR и CSR не должны конкурировать за начальное состояние. Сервер читает source, создаёт HTML и передаёт рядом сериализованный snapshot. Клиент принимает snapshot только после проверки его версии. При совпадении он пропускает начальный fetch. При несовпадении он сначала фиксирует проблему и останавливает неявную мутацию. Новый запрос допустим только как явно выбранное восстановление.</p>\n<h2>Механизм: четыре фазы и четыре владельца</h2>\n<p>SSR — это серверный запрос и HTML, который браузер получает первым. HTML показывает результат, но сам по себе не говорит клиентскому коду, из какого чтения он появился. Поэтому ответ должен передать и snapshot начального состояния. Snapshot — не общий кеш приложения. Это данные для конкретного server response.</p>\n<p>Hydrate связывает клиентскую логику с уже существующей разметкой. На этой границе нужно сравнить маркер версии HTML и версию snapshot. Версия может быть номером ревизии, ETag, версией набора фильтров или другим значением, которое сервер умеет получить вместе с данными. Нельзя сравнивать только время запроса: два чтения могут иметь одинаковую секунду, но разные права, locale или feature flags.</p>\n<div class='table-scroll'><table><caption>Кто владеет данными на первом проходе</caption><thead><tr><th scope='col'>Фаза</th><th scope='col'>Владелец</th><th scope='col'>Вход</th><th scope='col'>Допустимое действие</th><th scope='col'>Ошибка</th></tr></thead><tbody><tr><td>SSR/source</td><td><code>server-request</code></td><td>Запись и её версия</td><td>Прочитать source и создать HTML</td><td>HTML без понятной версии</td></tr><tr><td>Ответ</td><td><code>serialized-snapshot</code></td><td>Payload и версия SSR</td><td>Передать initial state рядом с HTML</td><td>Версия потерялась при сериализации</td></tr><tr><td>Hydrate</td><td><code>browser-transition</code></td><td>Версия разметки и snapshot</td><td>Сравнить до изменения состояния</td><td>Разные версии приняты молча</td></tr><tr><td>Client fetch</td><td><code>browser-transition</code></td><td>Явное решение после проверки</td><td>Пропустить запрос или начать recovery path</td><td>Fetch запускается на каждый mount</td></tr></tbody></table></div>\n<h2>Минимальный пример с версией</h2>\n<p>Ниже — учебная JavaScript-модель. Она работает только с объектами и JSON. В ней нет React, Next.js, DOM, сети, таймера и реального браузерного hydrate. Пример показывает порядок владения данными и помогает написать проверку границы. Он не доказывает LCP, latency или поведение конкретного hook.</p>\n<pre><code>const source = {\n id: 'entry-42',\n version: 'entry-r7',\n title: 'Training entry',\n status: 'published',\n};\n\nconst snapshot = JSON.stringify({\n schema: 'ssr-snapshot-v1',\n version: source.version,\n payload: {\n id: source.id,\n title: source.title,\n status: source.status,\n },\n});\n\nfunction planHydration(markupVersion, serializedSnapshot) {\n const parsed = JSON.parse(serializedSnapshot);\n\n if (markupVersion !== parsed.version) {\n return {\n outcome: 'mismatch-recorded-before-mutation',\n diagnostic: {\n expectedMarkupVersion: markupVersion,\n serializedVersion: parsed.version,\n },\n clientFetch: { performed: false, action: 'not-started' },\n };\n }\n\n return {\n outcome: 'snapshot-accepted-without-second-fetch',\n stateOwner: 'serialized-snapshot',\n initialState: parsed.payload,\n clientFetch: {\n performed: false,\n action: 'skipped-snapshot-version-matched',\n },\n };\n}\n\nconst matching = planHydration(source.version, snapshot);\nconsole.log(matching.outcome);\n// snapshot-accepted-without-second-fetch\n\nconst mismatch = planHydration('entry-r8', snapshot);\nconsole.log(mismatch.diagnostic);\n// { expectedMarkupVersion: 'entry-r8', serializedVersion: 'entry-r7' }</code></pre>\n<p>В совпадающей ветке snapshot становится входом для initial state. Второй запрос не стартует только потому, что его запуск запрещает правило, а не потому, что эффект случайно выполнился в нужном порядке. В ветке mismatch код не подменяет старые данные новым ответом. Он сохраняет обе версии и оставляет recovery path следующему слою.</p>\n<figure><img src='/assets/editorial/2022/ssr-csr-lifecycle-2022.svg' alt='Жизненный цикл SSR и CSR: server request создаёт HTML и snapshot, hydrate сравнивает версии, а client fetch пропускается при совпадении и останавливается при mismatch.' loading='lazy' /><figcaption>Граница между server request, сериализацией, hydrate и client fetch. Красная ветка означает диагностируемое несовпадение, а не автоматическое обновление.</figcaption></figure>\n<h2>Диагностика: симптом → причина → проверка → действие</h2>\n<div class='table-scroll'><table><caption>Что делать при наблюдаемом симптоме</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Причина</th><th scope='col'>Проверка</th><th scope='col'>Действие</th></tr></thead><tbody><tr><td>После hydrate повторяется запрос за той же записью</td><td>Клиент создаёт пустой initial state и не читает snapshot</td><td>Сопоставить время HTML, snapshot и первый fetch</td><td>Передать snapshot в initial state; fetch запускать только после явного условия</td></tr><tr><td>Текст меняется сразу после загрузки</td><td>HTML и snapshot пришли из разных чтений source</td><td>Записать обе версии и входы: user, locale, flags</td><td>Исправить общий источник либо остановить переход при mismatch</td></tr><tr><td>Hydrate выдаёт предупреждение</td><td>Клиент строит другую разметку</td><td>Сравнить server markup и первый client render без fetch</td><td>Убрать нестабильное значение из рендера или передать его через snapshot</td></tr><tr><td>После «исправления» причина не видна</td><td>Автоматический fetch маскирует расхождение</td><td>Временно записать markupVersion, serializedVersion и action</td><td>Сначала сохранить diagnostic, затем выбрать refresh или сообщение</td></tr></tbody></table></div>\n<p>Проверка должна охватывать не только версию записи. Если HTML зависит от пользователя, языка, часового пояса, прав или эксперимента, эти входы тоже входят в контракт. Одинаковый <code>entry-r7</code> не означает одинаковый результат для двух пользователей. Полезный snapshot хранит либо нормализованные входы, либо достаточно данных, чтобы клиент мог проверить их отдельно.</p>\n<h2>Порядок внедрения</h2>\n<ol><li>Зафиксируйте симптом в браузере: URL, время первого HTML, повторный запрос, видимое изменение и входы страницы.</li><li>Назовите владельца каждого значения: source, HTML, snapshot, client state и следующий fetch. Не называйте их одним словом «кеш».</li><li>Выберите версию, которую сервер получает рядом с source. Передайте её в snapshot вместе с payload и схемой формата.</li><li>Поставьте сравнение до mutation и до запуска client fetch. Запишите ожидаемую и фактическую версии.</li><li>Для совпадения примите snapshot как initial state и проверьте, что повторный fetch не выполняется.</li><li>Для mismatch остановите неявное обновление. Отдельно выберите recovery path: повторить чтение, показать ошибку или отрендерить управляемый fallback.</li><li>Добавьте тесты на совпадение, mismatch и изменение входа. В каждом тесте проверяйте не только итоговый экран, но и количество запросов.</li><li>Проверьте медленную сеть, отключённый JavaScript, устаревший HTML, разные locale и пользователя без права на часть данных.</li></ol>\n<h2>Отрицательный путь важнее счастливого</h2>\n<p>Соблазнительный обход — всегда делать client fetch после mount. Он действительно может показать свежую запись, но стирает вопрос о рассинхроне. Причина может быть в кеше HTML, неправильном ключе сериализации, смене пользователя, locale или feature configuration. Автоматический запрос превращает диагностируемый mismatch в незаметную замену состояния.</p>\n<p>Другой обход — принять snapshot без проверки. Он экономит запрос, но может показать устаревшие или чужие данные, если документ и payload прошли разные границы кеширования. Третий обход — сравнивать только HTML-строку. Это дорого, хрупко и не объясняет, какие входы породили результат. Сравнивайте версионированный контракт, а не случайный побочный признак.</p>\n<h2>Ограничения</h2>\n<p>SSR не делает страницу автоматически быстрой. Сервер может дольше формировать ответ, а сериализованный payload увеличивает документ. CSR остаётся правильным выбором для данных, которые появляются только после действия пользователя, зависят от браузерного API или часто меняются. Не стоит передавать в snapshot секреты, которые нельзя отдавать клиенту. Не стоит принимать данные из snapshot для другого пользователя или другого набора прав.</p>\n<p>Версия snapshot не заменяет авторизацию, схему данных и обработку ошибок сети. Она только обозначает связь между двумя фазами одного чтения. Для потокового SSR, частичной гидрации и нескольких независимых источников понадобятся отдельные версии и правила владельца. Эта статья не утверждает, что React или любой фреймворк сам реализует описанный протокол.</p>\n<h2>Критерий готовности</h2>\n<p>Решение готово, если на одном документе можно показать четыре значения: версию source, маркер HTML, версию snapshot и действие hydrate. При совпадении тест подтверждает, что snapshot принят, initial state построен из него, а повторный запрос не ушёл. При несовпадении тест подтверждает, что обе версии записаны, mutation не началась и recovery path назван явно. В браузере это видно в сетевом журнале и диагностической записи. Без этих доказательств «SSR без второго запроса» остаётся предположением.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://react.dev/reference/react-dom/server' target='_blank' rel='noopener noreferrer'>React: Server React DOM APIs</a> — официальная документация описывает серверный рендер начального HTML и границу server APIs.</li><li><a href='https://react.dev/reference/react-dom/client/hydrateRoot' target='_blank' rel='noopener noreferrer'>React: hydrateRoot</a> — официальная документация о присоединении React к серверной разметке и необходимости согласованного HTML.</li><li><a href='https://developer.mozilla.org/en-US/docs/Glossary/SSR' target='_blank' rel='noopener noreferrer'>MDN: Server-side rendering (SSR)</a> — актуальный официальный глоссарий сравнивает SSR, CSR и их совместное применение.</li></ul>"
}