Налаштування Dynamic Rendering для JavaScript-сайтів (Rendertron/Puppeteer)
Dynamic Rendering — підхід, при якому користувачам відправляється SPA, а пошуковим ботам — попередньо відрендерений HTML. Google офіційно визнає це прийнятним обходом, коли SSR недоступний.
Коли застосовувати Dynamic Rendering
- SPA на React/Vue/Angular без SSR
- Неможливість переписати застосунок під SSR
- Тимчасове рішення до впровадження SSR
- Окремі розділи сайту з проблемами індексації
Не застосовувати: якщо доступні SSR/SSG — Dynamic Rendering це другосортне рішення з накладними витратами.
Архітектура
Запит → nginx → Перевірка User-Agent
↓
Бот? → Prerender Service (Puppeteer)
↓
HTML снапшот → Бот
↓
Людина? → SPA bundle → Браузер
Rendertron: self-hosted prerender
git clone https://github.com/GoogleChrome/rendertron
cd rendertron
npm install
npm run build
# Docker
docker build -t rendertron .
docker run -p 3000:3000 rendertron
# nginx: визначити ботів та відправити на Rendertron
map $http_user_agent $prerender_ua {
~*(Googlebot|Bingbot|Slurp|DuckDuckBot|Baiduspider|YandexBot|LinkedInBot|
facebookexternalhit|Twitterbot|WhatsApp|TelegramBot|Slackbot) 1;
default 0;
}
map $args $prerender_args {
~*_escaped_fragment_= 1;
default 0;
}
map $prerender_ua$prerender_args $prerender {
"11" 1;
"10" 1;
"01" 1;
default 0;
}
server {
listen 80;
server_name company.com;
location / {
if ($prerender = 1) {
rewrite .* /index.html break;
proxy_pass http://rendertron:3000/render/https://company.com$request_uri;
}
try_files $uri /index.html;
}
}
Кастомний prerender на Puppeteer
Більше контролю над поведінкою рендерингу:
// prerender-server.js
const express = require('express')
const puppeteer = require('puppeteer')
const app = express()
let browser = null
async function getBrowser() {
if (!browser) {
browser = await puppeteer.launch({
headless: 'new',
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
]
})
}
return browser
}
app.get('/render', async (req, res) => {
const url = req.query.url
if (!url) return res.status(400).send('URL required')
try {
const browser = await getBrowser()
const page = await browser.newPage()
// Заблокувати медіа для прискорення
await page.setRequestInterception(true)
page.on('request', req => {
const type = req.resourceType()
if (['image', 'media', 'font'].includes(type)) {
req.abort()
} else {
req.continue()
}
})
await page.goto(url, {
waitUntil: 'networkidle0',
timeout: 30000
})
// Дочекатися конкретного елемента (якщо потрібно)
await page.waitForSelector('[data-ssr-ready]', { timeout: 10000 })
.catch(() => {}) // не фатально якщо елемента немає
const html = await page.content()
await page.close()
// Кешувати результат
cache.set(url, html, 3600) // 1 година
res.set('Content-Type', 'text/html')
res.send(html)
} catch (err) {
console.error(`Render failed for ${url}:`, err)
res.status(500).send('Render failed')
}
})
app.listen(3000)
Кешування результатів Prerender
Рендеринг кожної сторінки займає 1–5 секунд. Кеш обов'язковий:
const Redis = require('ioredis')
const redis = new Redis({ host: 'redis' })
async function cachedRender(url) {
const cacheKey = `prerender:${url}`
const cached = await redis.get(cacheKey)
if (cached) return cached
const html = await render(url)
await redis.setex(cacheKey, 3600, html)
return html
}
// Інвалідувати кеш при деплої
async function invalidateCache(pathPattern) {
const keys = await redis.keys(`prerender:*${pathPattern}*`)
if (keys.length) await redis.del(keys)
}
Middleware в Express/Fastify
// middleware/prerender.js
const botUserAgents = [
'googlebot', 'bingbot', 'yandexbot', 'baiduspider',
'facebookexternalhit', 'twitterbot', 'linkedinbot'
]
function isBot(userAgent) {
return botUserAgents.some(bot =>
userAgent.toLowerCase().includes(bot)
)
}
module.exports = async function prerenderMiddleware(req, res, next) {
const ua = req.headers['user-agent'] || ''
if (!isBot(ua)) return next() // людина — звичайна SPA
try {
const renderedHtml = await cachedRender(`https://${req.hostname}${req.originalUrl}`)
res.set('X-Prerendered', 'true')
res.send(renderedHtml)
} catch (err) {
// Fallback: відправити звичайну SPA (краще ніж 500)
next()
}
}
Prerender.io як SaaS альтернатива
# Використання Prerender.io без self-hosted рішення
location / {
if ($prerender = 1) {
proxy_pass https://service.prerender.io/https://company.com$request_uri;
proxy_set_header X-Prerender-Token "YOUR_TOKEN";
}
try_files $uri /index.html;
}
Моніторинг та відладка
# Перевірити що бот отримує відрендерений HTML
curl -A "Googlebot/2.1 (+http://www.google.com/bot.html)" \
https://company.com/product/42 | grep -c "product-title"
# Повинен повернути число > 0 (елементи знайдені в HTML)
// Додати маркер що сторінка повністю відрендерилася
// (для умови завершення Puppeteer)
window.prerenderReady = false
// ... після завантаження даних:
window.prerenderReady = true
// Puppeteer чекає:
await page.waitForFunction(() => window.prerenderReady === true)
Тривалість
Налаштування Rendertron або кастомного Puppeteer prerender з nginx та Redis-кешем — 2–3 робочих дні.







