Помимо Server-Side Rendering есть альтернативный способ отдать с сервера полностью готовый HTML-код - Prerendering. Сегодня мы рассмотрим, как настроить пререндеринг страниц при помощи библиотеки Puppeteer.
Что такое пререндеринг страниц
Как мы знаем, поисковые системы плохо индексируют Single Page Application сайты. По моим замечаниям, даже поисковой бот Google индексирует SPA сайт более медлительно. Пререндеринг - это процедура рендеринга Javascript перед отдачей странице поисковому боту или любому другому боту (социальных сетей и мессенджеров). Мы отслеживаем бота по заголовку User Agent, загружаем требуемую ему страницу и исполняем Javascript на ней с помощью консольного Chromium (Headless Chromium) и отдаем ему итоговый HTML-код. В итоге, поисковые боты индексируют наши страницы полностью, а также мы можем настраивать метатеги Opengraph для социальных сетей, чтобы наши ссылки в них отображались красиво.
Отличие пререндеринга от SSR
В отличии от пререндеринга SSR решает две задачи: во-первых помогает поисковым машинам индексировать сайты, и во-вторых - оптимизирует скорость загрузки страниц для клиента, ведь контент страницы отображается раньше, чем подгружается и исполняется Javascript-код. В отличие от пререндеринга, SSR, как правило, работает не только на поисковых ботов, а на всех клиентов. SSR более сложно настраивается, также является ресурсоемкой процедурой, так как чтобы отдать готовый HTML-код клиенту, серверу придется исполнить весь Javascript-код, который исполняется на Frontend-части. Пререндеринг - тоже ресурсоемкая процедура, но настраивается она проще, и теперь у нас есть такой замечательный сервис, как облачные функции, с помощью которого мы эту процедуру можем значительно удешевить. В итоге, если загрузка страницы для штатных клиентов для ресурса достаточна, и нет гонки за ее скоростью, пререндеринг, на мой взгляд, более целесообразен.
Создаем сервер для пререндеринга
Для пререндеринга мы будем использовать библиотеку Puppeteer. Для начала давайте создадим простой сервер на Fastify, который принимает запросы на пререндеринг и отдает готовый HTML-код:
const fastify = require('fastify');
const puppeteer = require('puppeteer');
const server = fastify();
server.get('/prerender', async (req, res) => {
const url = req.query.url;
if (!url) {
res.code(404);
return 'Not Found';
}
const browser = await puppeteer.launch({args: ['--no-sandbox']});
let page;
let statusCode = 200;
let body = '';
try {
page = await browser.newPage();
await page.goto(url, {waitUntil: "networkidle0"});
const resStatusCode = await page.evaluate(() => {
const metaTag = document.querySelector('head > meta[name="status-code"]');
return metaTag ? metaTag.content : undefined
});
if (resStatusCode) statusCode = Number(resStatusCode);
body = await page.content();
} catch(e) {
console.error(`Ошибка обработки страницы ${url}`, e);
} finally {
if (page) {
await page.close();
}
await browser.close();
}
if (!body) {
res.code(500);
return 'Internal server error';
}
res.code(statusCode);
res.header['Content-Type'] = 'text/html';
return body;
});
server.listen(3000, (err, address) => {
if (err) throw err;
console.log(`Server is now listening on ${address}`);
});
Сервер будет ожидать запросы по адресу http://localhost:3000/prerender?url=<адрес_страницы>
.
Вот ключевые особенности данного сервера:
- Сервер считает страницу загруженной, если на странице нет активных сетевых соединений в течение 500 миллисекунд. Это не подойдет для сайтов с непрерывными соединениями (например, с websocket-соединениями), но подойдет для сайтов с простыми http-запросами.
- Если на странице в
<head />
найден метатег “status-code”, статус страницы будет равен атрибуту “content” данного метатега. Тем самым, мы можем динамически указывать статус определенной страницы (например, для статуса 404 указав<meta name="status-code" content="404" />
).
Конечно, это не полная “инструкция к действию”, и такому серверу не хватает gracefull shutdown и настроек безопасности. Цель данного куска кода показать, как работает настройка пререндеринга.
Такой метод имеет весьма весомый недостаток - для него лучше выделить отдельную машину, так как в фоне будет запускаться пусть порезанный но chromium, который может отнимать немало ресурсов. Поэтому мы поступим хитрее и напишем облачную функцию.
Создаем облачную функцию для пререндеринга
Я представлю облачную функцию для Яндекс.Облака, но ее не сложно адаптировать под любой другой сервис. Для начала создадим файл index.js
внутри облачной функции со следующим содержимым:
const puppeteer = require('puppeteer');
module.exports.handler = async (event, context) => {
const url = event.queryStringParameters.url;
if (!url) {
return {
statusCode: 404,
body: 'Not Found'
};
}
const browser = await puppeteer.launch({args: ['--no-sandbox']});
let page;
let statusCode = 200;
let body = '';
try {
page = await browser.newPage();
await page.goto(url, {waitUntil: "networkidle0"});
const resStatusCode = await page.evaluate(() => {
const metaTag = document.querySelector('head > meta[name="status-code"]');
return metaTag ? metaTag.content : undefined
});
if (resStatusCode) statusCode = Number(resStatusCode);
body = await page.content();
} catch(e) {
console.error(`Ошибка обработки страницы ${url}`, e);
} finally {
if (page) {
await page.close();
}
await browser.close();
}
if (!body) {
return {
statusCode: 500,
body: 'Internal server error'
}
}
return {
statusCode,
headers: {
'Content-Type': 'text/html'
},
isBase64Encoded: false,
body
};
};
И рядом положим файл package.json
со следующим содержимым для того, чтобы функция подтянула зависимости:
{
"name": "prerender",
"version": "1.0.0",
"dependencies": {
"puppeteer": "13.5.2"
}
}
Тут всплывает преимущество библиотеки Puppeteer - она тянет Headless Chromium за собой, ведь мы не можем самостоятельно установить его внутри облачной функции.
Настройка проксирования
Итак, сервис имеем, но сами боты за контентом ходить не будут, поэтому давайте создадим проксирование на Nginx:
location / {
proxy_set_header Authorization <yc_prerender_token>;
set $prerender 0;
if ($http_user_agent ~* "googlebot|yandex|mail.ru_bot|telegrambot|vkshare|slackbot|whatsapp|pinterestbot|linkedinbot|facebookexternalhit|twitterbot|baiduspider") {
set $prerender 1;
}
if ($uri ~* "\.(js|css|xml|png|jpg|jpeg|pdf|txt|ico|zip|ttf|woff|svg|eot)") {
set $prerender 0;
}
resolver 8.8.8.8;
if ($prerender = 1) {
proxy_pass https://functions.yandexcloud.net/<yc_function_id>?url=$scheme://$http_host$request_uri;
}
root /path/to/dist;
try_files $uri $uri/ /index.html;
}
Если функция не публичная, вставляем токен авторизации вместо <yc_prerender_token>
. Также, вставляем идентификатор функции вместо <yc_function_id>
.
Заключение
Такой подход даст ряд плюсов, но и ряд минусов. Пробежимся по ним.
Плюсы:
- Не паримся с настройкой SSR. Все настраивается один раз. Просто пишем свободно код на любом доступном фреймворке и не беспокоимся о том, что что-то может быть “обрезано” для ботов.
- Можем без проблем настраивать все плюшки улучшения индексации с фронтенда, такие как Opengraph и Микроразметка.
- Подход с облачной функцией довольно дешевый.
Минусы:
- Иногда на холодную функция может подготавливать страницу более 3 секунд, о чем будут ругаться поисковые боты.
- С учетом более “затяжной” загрузки, не целесообразно пререндерить страницу для всех посетителей.
- Придется следить и добавлять user agent новых ботов, если они будут появляться.