Многопоточность в 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 (     <>       <script         type="application/ld+json"         dangerouslySetInnerHTML={{           __html: JSON.stringify({             "@context": "https://schema.org/",             "@type": "WebPage",             name: "Мониторинг сайтов | Проверятор",             description:               "Бесплатный мониторинг доступности сайтов 24\\7. Уведомим о сбоях в работе сайта в Telegram, по почте или SMS. Проверка сайта раз в минуту (включая Nginx, WordPress и другие сайты)",             url: "https://proverator.ru",           }),         }}       />        <HeaderComponent />        <div         style={{           width: "100%",           maxWidth: "100vw",           overflowX: "hidden",           position: "relative",         }}       >         <main>           <MainComponent />         </main>          <HowItWorksComponent />         <FeaturesComponent />         <AdvantagesComponent />         <Price />         <FooterComponent />          <MessageUsComponent />       </div>     </>   ); } 

Скрипт для стресс-теста

Скрипт для стресс-тестирования я написал на 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 <URL>")         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.

Надеюсь, мой эксперимент оказался для вас наглядным.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Задавались ли вопросом, как распараллелить NextJS?

16.67% Да, но не пытался параллелить2
8.33% Да, и даже внедрил в работу1
41.67% Нет5
33.33% Оптимизирую сайт по-другому4

Проголосовали 12 пользователей. Воздержались 2 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/848052/