Проблема SPA и client-side rendering
Разбираем корень SEO-боли современных JS-сайтов: контент, которого нет в исходном HTML.
Client-Side Rendering (CSR) — подход, при котором сервер отдаёт почти пустой HTML, а весь контент рисует JavaScript в браузере. Классика для SPA на React/Vue без серверного рендеринга.
Что отдаёт сервер в чистом SPA
В типичном CSR-приложении исходный HTML выглядит примерно так:
<body>
<div id="app"></div>
<script src="/bundle.js"></script>
</body>
Весь контент — заголовки, текст, ссылки, мета-теги — появляется только после того, как браузер скачает bundle.js, выполнит его и отрисует. Для пользователя с быстрым устройством это незаметно. Для поисковика — проблема.
Почему это бьёт по SEO
- Первая волна индексации пуста. Бот сразу видит
<div id="app"></div>— ни текста, ни заголовков, ни ссылок для дальнейшего обхода. - Рендер JS отложен. Вторая волна (исполнение JS) ставится в очередь и может выполниться спустя время — индексация задерживается.
- Бюджет и сбои. Если bundle тяжёлый, есть ошибка JS или внешний запрос не успел — бот может не получить контент вовсе.
- Мета-теги на клиенте.
title,description, OG, canonical, JSON-LD, проставленные через JS, могут быть не увидены ботами соцсетей (они JS не исполняют вообще).
Как увидеть проблему
Сравните «то, что в браузере» и «то, что в сыром HTML»:
# Сырой ответ сервера — то, что видит бот первой волной
curl -s https://example-spa.com/ | grep -i "<h1"
# Если ничего не нашлось, а в браузере h1 есть -> контент рисует JS
Как работает под капотом: модель двух волн
Смоделируем, что попадает в индекс при разных стратегиях рендеринга. Если контент только в JS, а вторая волна не дошла — индексируется пустота:
def indexed_content(raw_html, js_content, render_js_executed):
# raw_html - что в исходнике; js_content - что дорисует JS
visible = raw_html
if render_js_executed: # вторая волна выполнилась
visible = raw_html + js_content
return visible.strip() or "(пусто — нечего индексировать)"
csr_raw = "" # пустой div
csr_js = "Заголовок и текст статьи"
print("CSR, рендер НЕ дошёл:", indexed_content(csr_raw, csr_js, False))
print("CSR, рендер дошёл: ", indexed_content(csr_raw, csr_js, True))
print("SSR (контент в HTML):", indexed_content(csr_js, "", False))
Вывод:
CSR, рендер НЕ дошёл: (пусто — нечего индексировать) CSR, рендер дошёл: Заголовок и текст статьи SSR (контент в HTML): Заголовок и текст статьи
Видно: при SSR контент в индексе есть всегда; при CSR — только если повезло со второй волной. Решение этой проблемы (SSR/SSG) — в следующем уроке.
Частые ошибки
- «Google же умеет JS». Умеет, но с задержкой, ограничениями по бюджету и не во всех случаях; боты соцсетей и часть поисковиков — нет.
- Мета-теги и canonical только через JS — ненадёжно для индексации и шеринга.
- Контент, требующий клика/скролла — бот его не активирует.
- Ссылки через
onclickвместо<a href>— бот не пойдёт по ним, обход обрывается.
Итог
- В чистом CSR сервер отдаёт пустой HTML, а контент рисует JS — первая волна индексации пуста.
- Рендер JS отложен и не гарантирован; боты соцсетей JS не исполняют вовсе.
- Ссылки делайте через
<a href>, а ключевые мета-теги — в серверном HTML.