Команда JavaScript for Devs подготовила перевод статьи о том, сколько трафика реально выдерживает сайт на Next.js. Автор провёл нагрузочные тесты, сравнил VPS и выделенный сервер, проверил разницу между предрендерингом и SSR и сделал вывод: для сайтов с потенциальными всплесками трафика предрендеринг — спасение, а SSR может стать бутылочным горлышком.


Я часто говорил что-то вроде: «Предварительно отрендеренный сайт легко обслужит сотни одновременных пользователей», потому что, честно говоря, я никогда не видел, чтобы такой сайт падал.

Но сколько он реально может выдержать? Справится ли мой сайт, если вдруг попадёт на главную Hacker News? Как это сравнивается с серверным рендерингом? И стоит ли вообще из кожи вон лезть, чтобы избежать SSR?

Я пытался найти конкретные цифры по производительности Next.js, но достоверных данных оказалось удивительно мало. Поэтому я провёл собственные тесты на своём сайте — и результаты оказались неожиданными. А на следующий день мой пост про то, как Google Translate ломает React, оказался на главной Hacker News.

Как раз после того, как я выяснил (спойлер!) — мой сайт, скорее всего, бы не справился.

Предварительно отрендеренный сайт на VPS

Первым делом я хотел выяснить, выдержит ли мой сайт резкий наплыв посетителей, например, если он попадёт на главную Hacker News.

Для теста я написал простой скрипт нагрузочного тестирования на k6, чтобы замерить максимальное количество запросов в секунду, которые может обработать мой сервер. Скрипт был максимально прост: он многократно запрашивал одну и ту же страницу, дожидаясь завершения каждого запроса. Полное описание теста можно найти в приложении.

Надпись: «Сбавьте скорость — ограничение — 193 RPS»
Надпись: «Сбавьте скорость — ограничение — 193 RPS»

Запустив скрипт, я выяснил, что один инстанс Next.js, обслуживающий мою полностью предварительно отрендеренную главную страницу на X4 BladeVPS от TransIP, способен обрабатывать 193 запроса в секунду, при этом только 63% запросов отвечают быстрее, чем за 500 мс.

Важно подчеркнуть: каждый запрос в этом тесте — это лишь вызов одной предварительно отрендеренной страницы Next.js. Никакие дополнительные ассеты не запрашиваются. Реальный посетитель делает ещё 60+ запросов. То есть трёх одновременных посетителей может быть достаточно, чтобы сервер оказался на грани перегрузки.

Скажу честно, гораздо меньше моих ожиданий.

Анатомия визита

Чтобы понять, почему для загрузки сайта требуется более 60 запросов, давайте посмотрим, что именно происходит, когда кто-то открывает мою главную страницу:

  • DNS-запрос и SSL-рукопожатие.

  • Первичный запрос (1: 20,3 КБ): браузер запрашивает HTML-документ.

  • CSS (1: 1,6 КБ): браузер парсит HTML и запрашивает основной CSS-файл.

На этом этапе страница уже полностью стилизована и видна, а пользователь может взаимодействовать с большинством элементов — ещё до загрузки JavaScript. Единственное, чего пока нет:

  • Изображения в первом экране (5: 6,8 КБ): браузер загружает изображения, попадающие в зону видимости.

Обычно таких изображений немного, и общий объём к этому моменту составляет 50–100 КБ. Именно тогда посетитель видит «полноценную» страницу. Поскольку большинство интерактивных элементов реализованы нативно (средствами браузера), пользователь уже может взаимодействовать со страницей (например, переходить по ссылкам).

Остальные запросы «отложенные» — они низкого приоритета и не блокируют рендеринг. К ним относится большая часть трафика:

  • Аналитика (1: 2,5 КБ): запрос скрипта Plausible.

  • JavaScript (13: 176,8 КБ): загрузка JS, необходимого для основной страницы.

  • Отложенные изображения (1: 22,8 КБ): изображения, частично видимые в первом экране.

  • Плейсхолдеры данных (17: 11,9 КБ): данные-заглушки для изображений ниже первого экрана.

  • Favicon (2: 52,2 КБ): фавиконки разных размеров.

  • Предзагруженные ресурсы (22: 136,1 КБ): ресурсы для потенциальной навигации (чтобы переход был мгновенным).

В сумме получается 63 запроса и 434,5 КБ.

Это показывает, что Next.js генерирует заметно больше запросов и трафика по сравнению с классическим серверным рендерингом или статическим сайтом без Next.js. С одной стороны, это даёт мне, как разработчику, полный контроль над UX и отличный DX. С другой — пользователю и серверу приходится обрабатывать куда больше трафика. Справедливости ради, <500 КБ для современного сайта не так уж и много, особенно учитывая, что страница становится доступной к взаимодействию уже после первых 100 КБ.

React Server Components (RSC) могли бы немного улучшить ситуацию, убрав часть клиентского JavaScript. К сожалению, технология пока ещё сырая и содержит много проблем. Я готов мириться с этим ради максимальной производительности, но главный блокер для меня — отсутствие поддержки CSS-in-JS. Как только это исправят (например, после выхода PigmentCSS), я перейду на RSC без раздумий.

Масштабирование

После разочаровывающих результатов на VPS я попробовал масштабировать свой Docker-контейнер (docker compose up -d --scale main=2 main). Вместо одного контейнера с одним процессом Node.js, обрабатывающим все запросы, теперь у меня был по одному процессу на каждое ядро CPU (всего 2 ядра).

Масштабирование контейнера сайта
Масштабирование контейнера сайта

Примечание: Я использую nginx-proxy перед своими Docker-контейнерами. Это отличная штука: если несколько контейнеров слушают один и тот же домен, она автоматически балансирует нагрузку.

Эффект оказался минимальным. VPS теперь мог обрабатывать 275 запросов в секунду (+42,49%), но только 76% запросов укладывались в 500 мс. Этого всё ещё явно недостаточно, чтобы пережить настоящий «hug of death». А мне совсем не хочется, чтобы сайт лег.

Я также протестировал другие файлы, чтобы понять, как сервер справится с дополнительными 60+ запросами. Вот результаты:

Файл

RPS

Узкое место

Главная (20,3 КБ)

275 RPS

CPU

Статья в блоге (13,9 КБ)

440 RPS

CPU

Большой JS-чанк (46,8 КБ)

203 RPS

CPU

Средний JS-чанк (6,1 КБ)

833 RPS

CPU

Маленький JS-чанк (745 B)

1 961 RPS

CPU

Изображение (17,7 КБ)

1 425 RPS

CPU

Статически сгенерированный RSS-фид (2,4 КБ)

849 RPS

CPU

Примечание:
Интересный момент: масштабирование дало лишь +42,49% прироста RPS на главной, потому что Next.js уже выполняет базовый многопоточный GZIP-компресс. То есть даже один контейнер немного использовал второе ядро CPU ещё до масштабирования.

Неожиданный всплеск трафика

Как назло, на следующий день после того, как я выяснил, что мой сервер вряд ли переживёт серьёзный «hug of death», моя статья Everything about Google Translate crashing React (and other web apps) попала на главную страницу Hacker News.

Пост отображался как опубликованный час назад 14-го числа, хотя на самом деле был выложен двумя днями ранее.
Пост отображался как опубликованный час назад 14-го числа, хотя на самом деле был выложен двумя днями ранее.

Это стало полной неожиданностью — я опубликовал статью два дня назад. Как оказалось, Hacker News может поднимать старые посты на главную, давая им второй шанс. Я был в восторге, что моя статья (над которой я долго работал) привлекла внимание, но тот факт, что это случилось сразу после того, как я понял, что сайт может не выдержать нагрузку, был особенно стрессовым.

К счастью, трафик оказался не таким высоким, как я ожидал от главной Hacker News, и мой VPS выдержал. Но это заставило меня задуматься: насколько разрушительным может быть настоящий «hug of death»? Эту тему я хочу разобрать в отдельной статье — с цифрами и статистикой по собственному опыту.

Поиск замены

Разочаровавшись в производительности VPS, я начал искать лучшее решение.

Cloudflare

Самый простой вариант — поставить Cloudflare перед моим сервером и заставить его агрессивно кэшировать статический контент. Я уже использовал такую схему: когда я запускал WoWAnalyzer, один сервер вместе с Cloudflare без труда обслуживал более 550 000 уникальных посетителей в месяц.

Звучит отлично: с Cloudflare моему серверу пришлось бы обрабатывать только первый запрос, а остальные 60 брала бы на себя сеть Cloudflare. Поскольку мой VPS уже выдерживает 200 посетителей в секунду, проблема была бы решена.

Но, к сожалению, для меня Cloudflare — не вариант. Я забочусь о вашей (и своей) конфиденциальности и не хочу, чтобы Cloudflare сидел между моими пользователями и сайтом, логировал каждый запрос и потенциально отслеживал людей по всему интернету.

Я хочу использовать исключительно сервисы, расположенные в ЕС.

Vercel

Ещё один очевидный вариант — Vercel (или его европейский аналог). Но честно говоря, для меня это звучит как кошмар. Это недёшево, а модель ценообразования «на сайт» означает, что я либо окажусь навсегда привязан к платформе, либо придётся разбирать всё при закрытии проекта. Плюс непредсказуемые лимиты и дополнительные платежи буквально за всё, за что только можно брать деньги. Это превращает «вирусный успех» из технической задачи в стресс о том, сколько придёт в следующем счёте (а это может быть более $100 000!). В моём нынешнем сетапе стоимость фиксированная — и это мне очень нравится.

Твит с неожиданным скачком стоимости до $96 280 на Vercel. Подобных историй — много.
Твит с неожиданным скачком стоимости до $96 280 на Vercel. Подобных историй — много.

Примечание: Я действительно плачу за Plausible, который тоже имеет тарификацию по использованию. Но здесь я не против платить по трём причинам: их ценовые ступени предсказуемы и разумны, они не берут доплаты за разовые всплески трафика, а сам сервис не критичен — я могу отказаться от него в любой момент. Возможно, в будущем попробую развернуть его локально, чтобы сэкономить, но в прошлый раз, когда я пытался сделать это с похожим продуктом аналитики, мне так и не удалось заставить его обрабатывать хотя бы часть трафика, который на него шёл.

Домашний сервер

Следующий вариант — домашний сервер, но для меня это не вариант. У меня нет оптики, чтобы обеспечить нормальное время отклика, я не хочу, чтобы мой IP был публичным, он ещё и динамический, а интернет и электричество не настолько надёжные, какими я хотел бы видеть их для сервера. Для вас это может быть отличным решением, особенно если критичность данных невысокая и вы готовы использовать Cloudflare.

VPS

Наверняка есть более быстрые VPS за те же деньги или даже дешевле, но я не хочу тратить время на исследование провайдеров, проверять, находятся ли они в ЕС, а потом платить только ради тестирования. Да и вряд ли другой VPS дал бы радикально лучшие результаты.

Выделенный сервер

С учётом всех этих ограничений выделенный сервер выглядит самым подходящим вариантом. Да, для этого сайта это немного перебор, но он ведь не единственный, что у меня есть. Мне нужно хорошее место для моих проектов, и я хочу, чтобы порог запуска нового был как можно ниже. Сервер, на котором я могу свободно экспериментировать, — это идеально. Он снимает барьеры для творчества.

К тому же полезно держать навык в тонусе.

Выделенный сервер

Много лет я арендовал выделенный сервер у So You Start, бюджетного бренда OVH, чтобы хостить WoWAnalyzer. В связке с Cloudflare этот сервер без проблем обслуживал более 550 000 уникальных посетителей в месяц, обрабатывая примерно 75 миллионов запросов и не напрягаясь. Поэтому я решил снова попробовать их.

Их серверы очень доступны по цене. Да, они используют более старое железо OVH (2+ лет), но соотношение цена/качество отличное. После быстрого сравнения я выбрал самый дешёвый вариант: 36 евро в месяц за Intel Xeon-E 2136 с 6 ядрами/12 потоками — всего на 8 евро дороже, чем стоил VPS.

Полные характеристики выделенного сервера
Полные характеристики выделенного сервера

Это обновление дало колоссальную разницу.

Запустив 12 инстансов (по одному на поток), сервер смог обрабатывать 2 330 запросов в секунду к главной странице (99% из них отвечали быстрее, чем за 500 мс).

Я также провёл серию тестов для других файлов, чтобы сравнить их производительность:

Файл

RPS

Разница

Узкое место

Главная (20,3 КБ)

2 330 RPS

+747,27%

CPU

Статья в блоге (13,9 КБ)

3 950 RPS

+797,73%

CPU

Большой JS-чанк (46,8 КБ)

1 249 RPS

+515,27%

Сеть

Средний JS-чанк (6,1 КБ)

7 627 RPS

+815,61%

CPU

Маленький JS-чанк (730 B)

16 175 RPS

+724,83%

CPU

Изображение (17,7 КБ)

3 216 RPS

+125,68%

Сеть

Статически сгенерированный RSS-фид (2,4 КБ)

9 395 RPS

+1006,60%

CPU

На этот раз некоторые файлы упёрлись в сетевые ограничения. У бюджетных провайдеров, таких как So You Start, обычно ограничена пропускная способность — в данном случае максимум 500 Мбит/с.

Тем не менее, этого более чем достаточно для любых реальных нагрузок.

Средний размер файла примерно равен «среднему JS-чанку» (6,1 КБ). Значит, сервер способен обрабатывать около 7 600 запросов в секунду. Если разделить это на 60+ запросов, необходимых для загрузки главной страницы, получаем стабильный поток более 125 посетителей в секунду.

А статьи в блоге требуют меньше запросов (меньше изображений), так что на практике сервер выдержит ещё больше.

Это конфигурация, на которую я могу положиться.

Производительность SSR

Теперь, когда у нас есть хорошая база для сравнения производительности статического сайта, пора ответить на другой важный вопрос: как серверный рендеринг (SSR) сравнивается с предрендерингом?

Сложность тестирования SSR в том, что нужно решить, что именно измерять. SSR-страницы часто обращаются к базе данных (например, чтобы увеличить счётчик просмотров), вызывают API (например, поиск по Algolia) или обрабатывают пагинацию. Если учитывать это, тест будет измерять в основном накладные расходы этих интеграций, а не сам SSR.

Чтобы дать SSR лучший шанс, я сосредоточился только на том, что происходит при рендеринге React-страницы с небольшой синхронной логикой на сервере — без дополнительных API или базы данных.

Каждый тестовый кейс немного отличался:

  • Главная просто рендерит дерево компонентов React на сервере.

  • Статья в блоге делает немного логики (вычисляет связанные статьи) перед рендерингом.

  • RSS-фид генерируется динамически с помощью библиотеки rss.

SSR может по-разному влиять на эти кейсы, поэтому я ожидал интересных различий.

Результаты:

Файл

RPS

Разница

Узкое место

Главная (20,3 КБ)

271 RPS

+88,37%

CPU

Статья в блоге (13,9 КБ)

884 RPS

+77,62%

CPU

Статически сгенерированный RSS-фид (2,3 КБ)

4 521 RPS

+51,88%

CPU

Честно говоря, мне понадобилось несколько минут, чтобы переварить эти цифры.

На первый взгляд 271 RPS на главной не кажется катастрофой. Особенно учитывая, что статические ассеты (CSS, JS, картинки) всё так же отдаются статически, и их RPS почти не меняется.

Но если подумать, то это всё, что способен выдать выделенный сервер, на котором больше ничего не крутится. Это почти на 90% меньше запросов в секунду, чем при статической генерации. И, что хуже всего, лишь 74% запросов к главной укладывались в 500 мс — медианное время ответа составило 1,51 секунды, а каждый десятый запрос занимал более 3 секунд.

SSR катастрофически медленный.

Для полноты картины я запустил те же тесты на VPS:

Файл

RPS

Разница

Узкое место

Главная (20,3 КБ)

34 RPS

+87,64%

CPU

Статья в блоге (13,9 КБ)

95 RPS

+78,41%

CPU

Статически сгенерированный RSS-фид (2,3 КБ)

490 RPS

+42,29%

CPU

Ай.

Цифры ясно показывают: использовать SSR на странице, от которой вы ждёте большой трафик — это просто приглашение к проблемам.

Заключительные мысли

Всё началось с простого вопроса: выдержит ли мой сайт резкий всплеск трафика? Я всегда считал, что предрендеринг справится с чем угодно, но рад, что проверил это на практике. Оказалось, мой первоначальный VPS был далеко не таким надёжным, как я думал — всего три посетителя в секунду могли бы довести его до предела. Совсем не то, что я ожидал.

Выделенный сервер оказался настоящим апгрейдом по сравнению с VPS. И хотя стоит он ненамного дороже, ограничение по пропускной способности может стать скрытым узким местом. Нет смысла платить за более мощный CPU, если сеть не успевает за сервером.

Сейчас я думаю, что моя конфигурация способна выдержать больше, чем мне когда-либо реально понадобится.

Хотя, если быть честным, это всё ещё лишь предположение. Я всё ещё не знаю наверняка, насколько разрушительным может быть настоящий «hug of death». Но это тема для отдельной статьи — там же я поделюсь статистикой со своего выхода на главную Hacker News.

Что касается SSR, теперь у нас есть чёткие цифры, подтверждающие: предрендеринг масштабируется значительно лучше.

Мне всё ещё любопытно, насколько дальше можно разогнать сервер — без Next.js. В одной из следующих статей я проверю, изменится ли ситуация, если заменить сервер Next.js на Nginx и оптимизировать сжатие. Это покажет, насколько сам сервер Next.js эффективен.

Приложение

Изначально я планировал подробно рассказать обо всех инструментах, которые использовал, и обо всех результатах тестов, но статья и так получилась слишком длинной (мне пришлось разбить её на три части), поэтому я сократил этот раздел.

Скрипт k6:

import http from 'k6/http'
import { check } from 'k6'

const url = 'https://example.com'
// VUs are set to the amount needed to achieve 99% on the <500ms response time
// with a minimum of 200 to simulate a realistic peak amount of users.
// More VUs does not always mean more RPS, but it can lead to slower responses.
const vus = 200

export const options = {
  // Skip decompression to greatly reduce CPU usage. This prevents throttling on
  // my i9 MBP, making tests more reliable.
  discardResponseBodies: true,
  scenarios: {
    rps: {
      executor: 'ramping-vus',
      stages: [
        // A quick ramp up to max VUs to simulate a sudden spike in traffic.
        // "constant-vus" leads to connection errors (DDOS protection?), so this
        // ramp up is a bit more robust and realistic.
        { duration: '5s', target: vus },
        { duration: '1m', target: vus },
      ],
      // Interrupt tests running longer than 1sec at the end of the test so that
      // they don't skew the results.
      gracefulStop: '1s',
    },
  },
}

export default function () {
  const res = http.get(url, {
    headers: {
      'Accept-Encoding': 'br, gzip',
      accept: 'image/webp',
    },
  })

  check(res, {
    'status is 200': (r) => r.status === 200,
    'response time is below 500ms': (r) => r.timings.duration < 500,
  })
}

Среда тестирования и наблюдения

  • Тесты проводились на топовой версии Intel MacBook Pro 2019 года, подключённом через Ethernet (USB-C адаптер), со скоростью загрузки ~760 Мбит/с, что выше пропускной способности выделенного сервера.

  • Сетевые адаптеры могут стать узким местом при бенчмарках — я убедился, что мой адаптер не ограничивает производительность.

  • Между тестами я давал ноутбуку остыть, чтобы свести влияние троттлинга к минимуму (скорее всего, оно было незначительным).

  • Большинство тестов я запускал минимум по три раза (а скорее всего, больше) из-за проблем с тестированием или изменений скрипта. Результаты оказались довольно стабильными.

  • Я сосредоточился на ответах <500 мс, потому что это примерно максимум терпения, который у меня есть, когда я открываю новый сайт впервые.

  • OVH заявляет, что «burst доступен для поглощения разовых пиков трафика», но я не заметил никаких признаков этой функции.

  • Я делал тесты сжатия и с Nginx, и результаты были интересные (но не такие значительные, как можно подумать) — поделюсь ими отдельно.

  • Я также исследовал возможный масштаб «hug of death» с Hacker News и Reddit, но вырезал этот раздел, чтобы статья не разрослась ещё больше. Вернусь к этому позже (надеюсь, в следующей статье, но не обещаю).

Тестируемые файлы

Я тестировал следующие файлы и URL. Хэши могли измениться, но аналогичные файлы можно найти на сайте:

Пожалуйста, не используйте эти URL для своих нагрузочных тестов.

Русскоязычное JavaScript сообщество

Друзья! Эту статью перевела команда «JavaScript for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Frontend. Подписывайтесь, чтобы быть в курсе и ничего не упустить!