Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
На дворе 2023 год, и мы выпустили Node.js v20. Это значительное достижение, и цель этой статьи — использовать научную оценку состояния производительности Node.js.
Все результаты бенчмарков содержат воспроизводимый пример и описание аппаратного обеспечения. Чтобы уменьшить количество шума для постоянных читателей, воспроизводимые шаги будут свернуты в начале всех разделов.
Цель этой статьи - предоставить сравнительный анализ различных версий Node.js. Она подчеркивает улучшения и недостатки, а также дает представление о причинах этих изменений, не проводя никаких сравнений с другими рантаймами JavaScript.
Для проведения эксперимента мы использовали Node.js версий 16.20.0, 18.16.0 и 20.0.0 и разделили сьюты бенчмарков на три отдельные группы:
Node.js Internal Benchmark (внутренний бенчмарк)
Учитывая значительный размер и отнимающий много времени характер сьюта бенчмарка Node.js, я выбрал образцы, которые, по моему мнению, оказывают большее влияние на разработчиков и конфигурации Node.js, например, чтение файла размером 16 МБ с помощью fs.readfile. Эти бенчмарки сгруппированы по модулям, таким как fs и streams. Для получения дополнительной информации о сюьте бенчмарков Node.js, пожалуйста, обратитесь к исходному коду Node.js.
nodejs-bench-operations
Я занимаюсь сопровождением репозитория под названием nodejs-bench-operations, который содержит бенчмарки для всех основных версий Node.js, а также последние три выпуска каждой линейки версий. Это позволяет легко сравнивать результаты между различными версиями, например, Node.js v16.20.0 и v18.16.0, или v19.8.0 и v19.9.0, с целью выявления регрессий в кодовой базе Node.js. Если вы заинтересованы в сравнительном анализе Node.js, знакомство с этим репозиторием может оказаться весьма ценным (и не забудьте поставить ему звезду, если вы считаете его полезным).
HTTP-серверы (фреймворки)
Этот практичный HTTP бенчмарк отправляет значительное количество запросов по различным маршрутам, возвращая JSON, обычный текст и ошибки, используя в качестве ссылок express и fastify. Основная цель - определить, применимы ли результаты, полученные с помощью Node.js Internal Benchmark и nodejs-bench-operations, к обычным HTTP-приложениям.
Окружающая среда
Для выполнения этого бенчмарка использовался выделенный хост AWS со следующим оптимизированным для вычислений экземпляром:
c6i.xlarge (Ice Lake) 3,5 ГГц - оптимизированный для вычислений
4 vCPU
8 ГБ памяти
Canonical, Ubuntu, 22.04 LTS, amd64 jammy
1 Гб SSD тип диска
Внутренний бенчмарк Node.js
В этом бенчмарке были выбраны следующие модули/пространства имен:
fs — файловая система Node.js
events — классы событий Node.js EventEmitter / EventTarget
http — Node.js HTTP сервер + парсер
misc — время запуска Node.js с использованием child_processes и worker_threads + trace_events
module — Node.js module.require
streams — создание, удаление, чтение потоков Node.js и многое другое
url — парсер URL Node.js
buffers — операции с буферами Node.js
util — Node.js кодировщик/декодировщик текста
Использованные конфигурации доступны по адресу RafaelGSS/node#state-of-nodejs, а все результаты были опубликованы в основном репозитории: State of Node.js Performance 2023.
Методика проведения бенчмарка Node.js
Прежде чем представить результаты, важно объяснить статистический подход, использованный для определения достоверности результатов бенчмарков. Этот метод был подробно описан в предыдущей статье блога, с которой вы можете ознакомиться здесь: Подготовка и оценка бенчмарков.
Чтобы сравнить влияние новой версии Node.js, мы запустили каждый бенчмарк несколько раз (30) на каждой конфигурации и на Node.js 16, 18 и 20. В таблице результатов есть две колонки, которые требуют пристального внимания:
улучшение (improvement) — процент улучшения по сравнению с новой версией
достоверность (confidence) — сообщает нам, достаточно ли статистических данных для подтверждения улучшения.
Например, рассмотрим результаты следующей таблицы:
confidence improvement accuracy (*) (**) (***)
fs/readfile.js concurrent=1 len=16777216 encoding='ascii' duration=5 *** 67.59 % ±3.80% ±5.12% ±6.79%
fs/readfile.js concurrent=1 len=16777216 encoding='utf-8' duration=5 *** 11.97 % ±1.09% ±1.46% ±1.93%
fs/writefile-promises.js concurrent=1 size=1024 encodingType='utf' duration=5 0.36 % ±0.56% ±0.75% ±0.97%
Be aware that when doing many comparisons the risk of a false-positive result increases.
In this case, there are 10 comparisons, you can thus expect the following amount of false-positive results:
0.50 false positives, when considering a 5% risk acceptance (*, **, ***),
0.10 false positives, when considering a 1% risk acceptance (**, ***),
0.01 false positives, when considering a 0.1% risk acceptance (***)
Существует риск в 0,1%, что fs.readfile не улучшился с Node.js 16 до Node.js 18 (достоверность ***). Следовательно, мы вполне уверены в результатах. Структуру таблицы можно представить следующим образом:
fs/readfile.js
— бенчмарк файлconcurrent=1 len=16777216 encoding='ascii' duration=5
— опции бенчмарка. Каждый бенчмарк файл может иметь множество опций, в данном случае это чтение 1 параллельного файла с 16777216 байтами в течение 5 секунд с использованием ASCII в качестве метода кодировки.
Для любителей статистики, скрипт выполняет независимый/непарный 2-групповой t-тест с нулевой гипотезой, что производительность одинакова для обеих версий. Если p-значение меньше 0.05, в поле "Достоверность" появится звездочка. — Написание и запуск бенчмарков
Настройка бенчмарка
Клонируйте репозиторий форка Node.js
Проверьте ветку state-of-nodejs
Создайте двоичные файлы Node.js 16, 18 и 20
Запустите скрипт benchmark.sh
#1
git clone git@github.com:RafaelGSS/node.git
#2
cd node && git checkout state-of-nodejs
#3
nvm install v20.0.0
cp $(which node) ./node20
nvm install v18.16.0
cp $(which node) ./node18
nvm install v16.20.0
cp $(which node) ./node16
#4
./benchmark.sh
Файловая система
При апгрейде Node.js с 16 до 18 наблюдалось улучшение на 67% при использовании API fs.readfile с кодировкой ascii и на 12% при использовании utf-8.
Результаты бенчмарка показали, что при апгрейде Node.js с версии 16 до 18 улучшение API fs.readfile с кодировкой ascii составило около 67%, а при использовании utf-8 - около 12%. Файл, использованный для теста, был создан с помощью следующего сниппета:
const data = Buffer.alloc(16 * 1024 * 1024, 'x');
fs.writeFileSync(filename, data);
Однако при использовании fs.readfile с ascii на Node.js 20 наблюдалась регрессия в 27%. Об этой регрессии было сообщено команде Node.js Performance, и ожидается, что она будет исправлена. С другой стороны, fs.opendir, fs.realpath и fs.readdir показали улучшение от Node.js 18 до Node.js 20. Сравнение между Node.js 18 и 20 можно увидеть в приведенном ниже результате бенчмарка:
confidence improvement accuracy (*) (**) (***)
fs/bench-opendir.js bufferSize=1024 mode='async' dir='test/parallel' n=100 *** 3.48 % ±0.22% ±0.30% ±0.39%
fs/bench-opendir.js bufferSize=32 mode='async' dir='test/parallel' n=100 *** 7.86 % ±0.29% ±0.39% ±0.50%
fs/bench-readdir.js withFileTypes='false' dir='test/parallel' n=10 *** 8.69 % ±0.22% ±0.30% ±0.39%
fs/bench-realpath.js pathType='relative' n=10000 *** 5.13 % ±0.97% ±1.29% ±1.69%
fs/readfile.js concurrent=1 len=16777216 encoding='ascii' duration=5 *** -27.30 % ±4.27% ±5.75% ±7.63%
fs/readfile.js concurrent=1 len=16777216 encoding='utf-8' duration=5 *** 3.25 % ±0.61% ±0.81% ±1.06%
0.10 false positives, when considering a 5% risk acceptance (*, **, ***),
0.02 false positives, when considering a 1% risk acceptance (**, ***),
0.00 false positives, when considering a 0.1% risk acceptance (***)
Если вы используете Node.js 16, то можете ознакомиться следующим сравнением между Node.js 16 и Node.js 20
confidence improvement accuracy (*) (**) (***)
fs/bench-opendir.js bufferSize=1024 mode='async' dir='test/parallel' n=100 *** 2.79 % ±0.26% ±0.35% ±0.46%
fs/bench-opendir.js bufferSize=32 mode='async' dir='test/parallel' n=100 *** 5.41 % ±0.27% ±0.35% ±0.46%
fs/bench-readdir.js withFileTypes='false' dir='test/parallel' n=10 *** 2.19 % ±0.26% ±0.35% ±0.45%
fs/bench-realpath.js pathType='relative' n=10000 *** 6.86 % ±0.94% ±1.26% ±1.64%
fs/readfile.js concurrent=1 len=16777216 encoding='ascii' duration=5 *** 21.96 % ±7.96% ±10.63% ±13.92%
fs/readfile.js concurrent=1 len=16777216 encoding='utf-8' duration=5 *** 15.55 % ±1.09% ±1.46% ±1.92%
События
Класс EventTarget
показал наиболее значительное улучшение в части событий. Бенчмарк включал диспетчеризацию миллиона событий с помощью EventTarget.prototype.dispatchEvent(new Event('foo'))
.
Апгрейд с Node.js 16 до Node.js 18 может обеспечить улучшение производительности диспетчеризации событий почти на 15%. Но настоящий скачок происходит при переходе с Node.js 18 на Node.js 20, который может дать улучшение производительности до 200% при наличии только одного слушателя.
Класс EventTarget
является важнейшим компонентом Web API и используется в различных родительских фичах, таких как AbortSignal
и worker_threads
. В результате, оптимизация этого класса может потенциально повлиять на производительность этих фич, включая fetch и AbortController
. Кроме того, API EventEmitter.prototype.emit
также получил заметное улучшение примерно на 11,5% при сравнении Node.js 16 с Node.js 20. Полное сравнение приведено ниже для справки:
confidence improvement accuracy (*) (**) (***)
events/ee-emit.js listeners=5 argc=2 n=2000000 *** 11.49 % ±1.37% ±1.83% ±2.38%
events/ee-once.js argc=0 n=20000000 *** -4.35 % ±0.47% ±0.62% ±0.81%
events/eventtarget-add-remove.js nListener=10 n=1000000 *** 3.80 % ±0.83% ±1.11% ±1.46%
events/eventtarget-add-remove.js nListener=5 n=1000000 *** 6.41 % ±1.54% ±2.05% ±2.67%
events/eventtarget.js listeners=1 n=1000000 *** 259.34 % ±2.83% ±3.81% ±5.05%
events/eventtarget.js listeners=10 n=1000000 *** 176.98 % ±1.97% ±2.65% ±3.52%
events/eventtarget.js listeners=5 n=1000000 *** 219.14 % ±2.20% ±2.97% ±3.94%
HTTP
HTTP-серверы являются одним из наиболее существенных уровней усовершенствования в Node.js. Это не миф, что большинство приложений Node.js в настоящее время работают на HTTP-сервере. Поэтому любое изменение может легко рассматриваться как semver-major [семантическое объявление основной версии] и увеличить усилия для совместимого улучшения производительности.
Поэтому используемый HTTP-сервер — это http.Server, который на каждый запрос отвечает 4 чанками по 256 байт каждый, содержащими 'C', как вы можете видеть в этом примере:
http.createServer((req, res) => {
const n_chunks = 4;
const body = 'C'.repeat();
const len = body.length;
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Length': len.toString()
});
for (i = 0, n = (n_chunks - 1); i < n; ++i)
res.write(body.slice(i * step, i * step + step));
res.end(body.slice((n_chunks - 1) * step));
})
// See: https://github.com/nodejs/node/blob/main/benchmark/fixtures/simple-http-server.js
При сравнении производительности Node.js 16 и Node.js 18 заметно улучшение на 8%. Однако апгрейд с Node.js 18 на Node.js 20 привел к значительному улучшению на 96,13%.
Данные результаты были получены с помощью бенчмарк-метода test-double-http. Это простой скрипт Node.js для отправки HTTP GET-запросов:
function run() {
if (http.get) { // HTTP or HTTPS
if (options) {
http.get(url, options, request);
} else {
http.get(url, request);
}
} else { // HTTP/2
const client = http.connect(url);
client.on('error', () => {});
request(client.request(), client);
}
}
run();
При переходе на более надежные инструменты бенчмаркинга, такие как autocannon или wrk, мы наблюдали значительное снижение заявленного улучшения — с 96% до 9%. Это указывает на то, что предыдущий метод бенчмаркинга имел ограничения или ошибки. Однако фактическая производительность HTTP-сервера улучшилась, и нам необходимо тщательно оценить процент улучшения с помощью нового бенчмаркингового подхода, чтобы точно измерить достигнутый прогресс.
Следует ли мне ожидать улучшения производительности на 96%/9% в моем приложении Express/Fastify?
Безусловно, нет. Фреймворки могут не использовать внутренний HTTP API — это одна из причин, почему Fastify... быстрый! По этой причине в данном отчете был рассмотрен другой набор бенчмарков (3. HTTP-серверы).
Разное
Согласно нашим тестам, скрипт startup.js продемонстрировал значительное улучшение жизненного цикла процессов Node.js, причем по сравнению с Node.js версии 18 до версии 20 наблюдалось увеличение на 27%. Оно еще более впечатляет в сравнении с Node.js версии 16, где время запуска сократилось на 34,75%!
Поскольку современные приложения все больше полагаются на бессерверные системы, сокращение времени запуска стало решающим фактором в повышении общей производительности. Стоит отметить, что команда Node.js постоянно работает над оптимизацией этого аспекта платформы, о чем свидетельствует наша стратегическая инициатива: https://github.com/nodejs/node/issues/35711.
Сокращение времени запуска не только идет на пользу бессерверным приложениям, но и повышает производительность других приложений Node.js, которые полагаются на быстрое время загрузки. В целом, эти обновления демонстрируют стремление команды Node.js повысить скорость и эффективность платформы для всех пользователей.
$ node-benchmark-compare compare-misc-16-18.csv
confidence improvement accuracy (*) (**) (***)
misc/startup.js count=30 mode='process' script='benchmark/fixtures/require-builtins' *** 12.99 % ±0.14% ±0.19% ±0.25%
misc/startup.js count=30 mode='process' script='test/fixtures/semicolon' *** 5.88 % ±0.15% ±0.20% ±0.26%
misc/startup.js count=30 mode='worker' script='benchmark/fixtures/require-builtins' *** 5.26 % ±0.14% ±0.19% ±0.25%
misc/startup.js count=30 mode='worker' script='test/fixtures/semicolon' *** 3.84 % ±0.15% ±0.21% ±0.27%
$ node-benchmark-compare compare-misc-18-20.csv
confidence improvement accuracy (*) (**) (***)
misc/startup.js count=30 mode='process' script='benchmark/fixtures/require-builtins' *** -4.80 % ±0.13% ±0.18% ±0.23%
misc/startup.js count=30 mode='process' script='test/fixtures/semicolon' *** 27.27 % ±0.22% ±0.29% ±0.38%
misc/startup.js count=30 mode='worker' script='benchmark/fixtures/require-builtins' *** 7.23 % ±0.21% ±0.28% ±0.37%
misc/startup.js count=30 mode='worker' script='test/fixtures/semicolon' *** 31.26 % ±0.33% ±0.44% ±0.58%
Этот бенчмарк довольно прост. Мы измеряем время, затраченное на создание нового [mode] (режим) с помощью заданного [script] (сценарий), где [mode] может быть:
process
— новый процесс Node.jsworker
— worker_thread (рабочий поток) Node.js.
А [script] делится на:
benchmark/fixtures/require-builtins
— скрипт, для которого необходимы все модули Node.jstest/fixtures/semicolon
— пустой скрипт, содержащий только одну ; (точку с запятой).
Этот эксперимент может быть легко воспроизведен с помощью hyperfine или time:
$ hyperfine --warmup 3 './node16 ./nodejs-internal-benchmark/semicolon.js'
Benchmark 1: ./node16 ./nodejs-internal-benchmark/semicolon.js
Time (mean ± σ): 24.7 ms ± 0.3 ms [User: 19.7 ms, System: 5.2 ms]
Range (min … max): 24.1 ms … 25.6 ms 121 runs
$ hyperfine --warmup 3 './node18 ./nodejs-internal-benchmark/semicolon.js'
Benchmark 1: ./node18 ./nodejs-internal-benchmark/semicolon.js
Time (mean ± σ): 24.1 ms ± 0.3 ms [User: 18.1 ms, System: 6.3 ms]
Range (min … max): 23.6 ms … 25.3 ms 123 runs
$ hyperfine --warmup 3 './node20 ./nodejs-internal-benchmark/semicolon.js'
Benchmark 1: ./node20 ./nodejs-internal-benchmark/semicolon.js
Time (mean ± σ): 18.4 ms ± 0.3 ms [User: 13.0 ms, System: 5.9 ms]
Range (min … max): 18.0 ms … 19.7 ms 160 runs