Дата, которая в браузере была вчера
Vue SSR писал 14 Jul в UTC. Клиент писал 13 Jul в Берлине. Гидратация падала, страница рисовалась дважды.
Плитки кейсов вспыхивали на первой отрисовке. Vue писал, что HTML сервера не совпал. Расхождение было календарным днём. У контейнера vue TZ=UTC. Intl.DateTimeFormat в Node печатал 14 Jul 2026. Браузер в Берлине печатал 13 Jul 2026 для того же ISO из API. Гидратация сравнивала текстовые узлы и сдавалась. Клиент рисовал дерево снова. TTFB выглядел нормально. Время до стабильной страницы нет.
Тот же класс ошибки бил по локали. SSR брал локаль из пути. Первый визит без cookie всё равно читал navigator.language на клиенте, если хелпер смотрел его в setup. На сервере en, в браузере ru, два словаря, гидратация падала.
Проблема была в форматировании одного момента в двух часах. Посетители видели вспышку и двойной рендер. Нужна строка с SSR в сериализованном состоянии и без Intl на mount.
Формат на сервере, в payload строка
PHP отдаёт published_at как ISO-8601 UTC. SSR-entry форматирует один раз под локаль запроса и кладёт строку в сериализованное состояние. Клиент печатает эту строку. Intl на mount не вызывает. Смена языка это новый URL и новый документ. Так i18n на этом сайте уже устроен.
- Даты, которым нужны часы посетителя, если такие есть, помечены client-only и в SSR HTML пустые.
- TZ процесса vue остаётся UTC. Угадать Europe/Berlin в Docker починит один офис и сломает краулер.
- Предупреждения гидратации в логе vue это провал деплоя, а не шум консоли.
Вспышка пропала. Документ в первом HTML это документ после гидратации. Двойной рендер был ценой форматирования даты в двух местах.
Какой опыт из этого
Гидратация сравнивает текст, не моменты. Два пояса это две строки даже при одном ISO.
Форматировать один раз на сервере. Везти строку. Intl на mount это второй календарь, который мне не нужен.
navigator.language в setup это расхождение локали на первом визите. Источник это локаль в пути.
