Многопоточность в NextJS: как запустить и нужно ли?
На определённом этапе своей карьеры я задался вопросом: может ли Next.js работать в многопоточном режиме? Оказалось, что нет. Это побудило меня разобраться, как можно организовать многопоточную работу Next.js и насколько это оправдано для сайтов с высокой нагрузкой.
Содержание
Какую страницу будем нагружать?
Для тестирования я использую страницу своего проекта по мониторингу падений сайтов с уведомлениями в Telegram. Основная страница сайта генерируется в статичную страницу (SSR), имеет картинки визуализации работы, на ней нет JS-интерактива. Одна из ключевых задач этой страницы — корректный рендеринг для SEO-оптимизации.
В статичном виде вес страницы составляет 71,2 КБ, а в полностью отрендеренном виде — 4,2 МБ.
Выглядит страница вот так:
Код страницы включает метаданные, Open Graph и HTML, написанный с использованием JSX (внутри используются такие же React-компоненты без интерактива, структурированные по секциям):
import { Metadata } from "next";
import HeaderComponent from "../pages-components/header/HeaderComponent";
import MainComponent from "../pages-components/main/MainComponent";
import FeaturesComponent from "../pages-components/features/FeaturesComponent";
import FooterComponent from "../pages-components/footer/FooterComponent";
import AdvantagesComponent from "../pages-components/advantages/AdvantagesComponent";
import Price from "../pages-components/price/Price";
import HowItWorksComponent from "../pages-components/how-it-works/HowItWorksComponent";
import { MessageUsComponent } from "@/util/components/MessageUsComponent";
export const metadata: Metadata = {
title: "Мониторинг сайтов | Проверятор",
description:
"Бесплатный мониторинг доступности сайтов 24\\7. Уведомим о сбоях в работе сайта в Telegram, по почте или SMS. Проверка сайта раз в минуту (включая Nginx, WordPress и другие сайты)",
keywords: "мониторинг сайтов, проверка доступности, проверка сбоев сайта",
icons: {
icon: "/favicon.ico",
},
alternates: {
canonical: `https://proverator.ru`,
},
openGraph: {
title: "Мониторинг сайтов | Проверятор",
description:
"Бесплатный мониторинг доступности сайтов 24\\7. Уведомим о сбоях в работе сайта в Telegram, по почте или SMS. Проверка сайта раз в минуту (включая Nginx, WordPress и другие сайты)",
url: "https://proverator.ru",
type: "website",
images: ["https://proverator.ru/banner.png"],
},
};
export default function Home() {
return (
<>
>
);
}
Скрипт для стресс-теста
Скрипт для стресс-тестирования я написал на Python с помощью ChatGPT o1. Запросы к сайту выполняются в 10 параллельных процессах (а не потоках, так как GIL не поддерживает «полноценную» многопоточность).
В течение минуты мы отправляем асинхронные запросы, а затем подсчитываем количество успешных ответов:
import asyncio
import aiohttp
import time
import multiprocessing
async def fetch(session, url, success_counter, end_time):
while time.time() < end_time:
try:
async with session.get(url) as response:
if response.status == 200:
with success_counter.get_lock():
success_counter.value += 1
except Exception:
pass # Ignore exceptions to continue the stress test
async def runner(url, success_counter, end_time):
async with aiohttp.ClientSession() as session:
await fetch(session, url, success_counter, end_time)
def process_function(url, success_counter, end_time):
asyncio.run(runner(url, success_counter, end_time))
def main(url):
success_counter = multiprocessing.Value('i', 0)
end_time = time.time() + 60 # Run for 1 minute
processes = []
for _ in range(10): # Up to 10 processes
p = multiprocessing.Process(target=process_function, args=(url, success_counter, end_time))
p.start()
processes.append(p)
for p in processes:
p.join()
print(f"Number of successful requests: {success_counter.value}")
if __name__ == "__main__":
import sys
if len(sys.argv) != 2:
print("Usage: python stress_test.py ")
sys.exit(1)
url = sys.argv[1]
main(url)
Конфигурация компьютера
Стресс-тест я запускаю на своём рабочем компьютере с процессором AMD Ryzen 9 7950X (16 ядер, 32 потока), 64 ГБ оперативной памяти и NVMe-диском. Операционная система — Windows 11.
Тестирование провожу локально, чтобы избежать ограничений по пропускной способности домашнего интернета и издержек коммуникации с сервером через интернет.
Стресс-тест в однопотоке
Собираю и запускаю сайт:
> npm run build
> npm run start
Запускаю тест в три итерации:
> python .\stresstest.py http://localhost:3000
> Number of successful requests: 94482
> python .\stresstest.py http://localhost:3000
> Number of successful requests: 92523
> python .\stresstest.py http://localhost:3000
> Number of successful requests: 93764
Загрузка компьютера во время теста (в среднем ~24% CPU, в простое ~2–7%):
Как запустить многопоточность?
Запуск многопоточного режима в Next.js оказался нетривиальной задачей. Я пробовал разные варианты с PM2, но безуспешно. После нескольких часов изучения нашёл статью на dev.to для старой версии Next.js 12.
Конечно, с первой попытки ничего не заработало, но после некоторых манипуляций удалось нащупать рабочий скрипт:
// start-multicore.js
const cluster = require("node:cluster");
const process = require("node:process");
const CPU_COUNT = require("os").cpus().length;
if (cluster.isPrimary) {
cluster.setupPrimary({
exec: require.resolve("next/dist/bin/next"),
args: ["start", ...process.argv.slice(2), "-p", "3000"],
stdio: "inherit",
shell: true,
});
for (let i = 0; i < CPU_COUNT; i++) {
cluster.fork();
}
cluster.on("exit", (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`, { code, signal });
});
}
Далее, запускаю NextJS на все ядра компьютера (получается 32 потока):
> node .\start-multicore.js
P.S. Тут учитываем, что и скрипт теста, и фоновые задачи будут всё на тех же физических ядрах.
Стресс-тест в многопотоке
Собираю и запускаю сайт:
> npm run build
> node .\start-multicore.js
Запускаю тест в многопотоке в три итерации:
> python .\stresstest.py http://localhost:3000
> Number of successful requests: 231428
> python .\stresstest.py http://localhost:3000
> Number of successful requests: 235704
> python .\stresstest.py http://localhost:3000
> Number of successful requests: 239138
Загрузка CPU во время теста (~44%):
Результаты
Результаты теста показали, что многопоточная версия обрабатывает примерно в 2.5 раза больше запросов, чем однопоточная. График для наглядности:
Однако стоит учесть несколько факторов:
что в фоне запущены другие программы;
во время теста скрипт и сайт работают на одном и том же компьютере;
несмотря на рост мощности в 32 раза (по количеству ядер), число обработанных запросов увеличилось всего в 2.5 раза.
Заключение
Стоит ли использовать многопоточность для Next.js? Вопрос неоднозначный. Я считаю, что это всё же не лучший подход.
В синтетическом тесте удалось увеличить количество обрабатываемых страниц в локальной сети. Но в реальных условиях серверы обычно менее мощные, и их пропускная способность варьируется от 100 Мбит/с до 1 Гбит/с.
Максимум, которого я достиг — около 250 000 запросов в минуту для статичной страницы без рендеринга. При этом в отрендеренном виде сайт весит 4.2 МБ.
Предположим, сайт идеально оптимизирован: вся статика вынесена в CDN, а сервер отдаёт 1 МБ данных на запрос. Даже допустим, что половина пользователей уже имеет закэшированную версию сайта, и фактически с сервера передаётся только 0.5 МБ на каждый запрос.
При пропускной способности 1 Гбит/с (125 МБ/с) сервер сможет обработать максимум 15 000 запросов в минуту (в сферически идеальных условиях). Мы всё ещё не приближаемся к пределам однопоточного Next.js. Да и большой вопрос, насколько многопоточный режим будет хорошо работать в связке с другими процессами, Nginx’ом и т.д.
Поэтому, если сайт растёт, разумнее начинать горизонтально масштабироваться. Всё-таки мы не хотим единую точку отказа, а хотим много серверов с load balancing’ом. В этой ситуации однопоточной версии Next.js будет достаточно. Так будет и надёжнее, и более рентабельно используем ресурсы.
Тем более, использование нестандартных скриптов — усложняет поддержку. С новой версией NextJS такой способ «распараллеливания работы» может перестать работать. Лучше отдать задачу оптимизации NextJS команде разработки NextJS.
Надеюсь, мой эксперимент оказался для вас наглядным.