На дворе 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, который содержит бенчмарки для всех основных версий 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
???? Warmup (подготовка) необходима для учета влияния кэша файловой системы.
Модуль trace_events
также претерпел заметный прирост производительности: при сравнении Node.js версии 16 с версией 20 улучшение составило 7%. Стоит отметить, что при сравнении Node.js версии 18 с версией 20 это улучшение было немного ниже — 2,39%.
Модуль
require()
(или module.require) долгое время был виновником медленного времени запуска Node.js. Однако последние улучшения производительности позволяют предположить, что эта функция также была оптимизирована. Между версиями Node.js 18 и 20 мы наблюдали улучшение на 4,20% при запросе файлов .js, 6,58% для файлов .json и 9,50% при чтении каталогов — все это способствует более быстрому запуску.
Оптимизация require()
крайне важна, поскольку данная функция активно используется в приложениях Node.js. Сократив время ее выполнения, мы можем значительно ускорить весь процесс запуска и улучшить пользовательский опыт.
Потоки
Потоки (streams) — это невероятно мощная и широко используемая функция Node.js. Однако между версиями Node.js 16 и 18 некоторые операции, связанные с потоками, стали выполняться медленнее. К ним относятся создание и удаление потоков Duplex, Readable, Transform и Writable, а также метод .pipe() для потоков Readable → Writable.
Приведенный ниже график иллюстрирует эту регрессию:
Однако в Node.js 20 эта регрессия pipe была уменьшена:
$ node-benchmark-compare compare-streams-18-20.csv
confidence improvement accuracy (*) (**) (***)
streams/creation.js kind='duplex' n=50000000 *** 12.76 % ±4.30% ±5.73% ±7.47%
streams/creation.js kind='readable' n=50000000 *** 3.48 % ±1.16% ±1.55% ±2.05%
streams/creation.js kind='transform' n=50000000 ** -7.59 % ±5.27% ±7.02% ±9.16%
streams/creation.js kind='writable' n=50000000 *** 4.20 % ±0.87% ±1.16% ±1.53%
streams/destroy.js kind='duplex' n=1000000 *** -6.33 % ±1.08% ±1.43% ±1.87%
streams/destroy.js kind='readable' n=1000000 *** -1.94 % ±0.70% ±0.93% ±1.21%
streams/destroy.js kind='transform' n=1000000 *** -7.44 % ±0.93% ±1.24% ±1.62%
streams/destroy.js kind='writable' n=1000000 0.20 % ±1.89% ±2.52% ±3.29%
streams/pipe.js n=5000000 *** 87.18 % ±2.58% ±3.46% ±4.56%
И как вы могли заметить, некоторые типы потоков (в частности, Transform) регрессируют в Node.js 20. Поэтому Node.js 16 по-прежнему имеет самые быстрые потоки — для этого конкретного бенчмарка, пожалуйста, не воспринимайте его результаты как утверждение "потоки Node.js в v18 и v20 такие медленные!". Это определенный бенчмарк, который может как повлиять, так и нет на вашу рабочую нагрузку. Например, если вы посмотрите на наивное сравнение в nodejs-bench-operations, то увидите, что следующий сниппет работает лучше на Node.js 20, чем его предшественники:
suite.add('streams.Writable writing 1e3 * "some data"', function () {
const writable = new Writable({
write (chunk, enc, cb) {
cb()
}
})
let i = 0
while(i < 1e3) {
writable.write('some data')
++i
}
})
Дело в том, что методы instantiation (инстанцирование) и destroy (уничтожение) играют важную роль в экосистеме Node.js. Таким образом, вполне вероятно, что это окажет негативное влияние на некоторые библиотеки. Тем не менее, эта регрессия внимательно отслеживается в Node.js Performance WG.
Обратите внимание, что читаемый асинхронный итератор становится немного быстрее (~6,14%) на Node.js 20.
URL
Начиная с Node.js 18, в нее была добавлена новая зависимость парсера URL — Ada. Это дополнение подняло производительность Node.js при парсинге URL на новый уровень. Некоторые результаты могут достигать 400%. Как обычный пользователь, вы можете не использовать его напрямую. Но если вы применяете HTTP-сервер, то вполне вероятно, что на него повлияет это улучшение производительности.
Набор бенчмарков URL довольно велик. По этой причине будут рассмотрены только результаты бенчмарка URL WHATWG.
url.parse()
и url.resolve()
являются устаревшими и наследуемыми API. Несмотря на то, что их использование считается рискованным для любого приложения Node.js, разработчики все еще применяют их. Цитирую документацию Node.js:
url.parse()
использует мягкий, нестандартный алгоритм для парсинга строк URL. Он подвержен таким проблемам безопасности, как подмена имени хоста и некорректная обработка имен пользователей и паролей. Не пользуйтесь им при ненадежных входных данных. CVE не выдаются для уязвимостей url.parse()
. Вместо этого используйте WHATWG URL API.
Если вам интересно узнать об изменениях производительности url.parse и url.resolve, ознакомьтесь с репозиторием State of Node.js Performance 2023.
Тем не менее, очень интересно посмотреть на результаты работы нового whatwg-url-parse:
Ниже приведен список URL-адресов, использованных для сравнительного анализа, которые были выбраны на основе конфигурации бенчмарка.
const urls = {
long: 'http://nodejs.org:89/docs/latest/api/foo/bar/qua/13949281/0f28b/' +
'/5d49/b3020/url.html#test?payload1=true&payload2=false&test=1' +
'&benchmark=3&foo=38.38.011.293&bar=1234834910480&test=19299&3992&' +
'key=f5c65e1e98fe07e648249ad41e1cfdb0',
short: 'https://nodejs.org/en/blog/',
idn: 'http://你好你好.在线',
auth: 'https://user:pass@example.com/path?search=1',
file: 'file:///foo/bar/test/node.js',
ws: 'ws://localhost:9229/f46db715-70df-43ad-a359-7f9949f39868',
javascript: 'javascript:alert("node is awesome");',
percent: 'https://%E4%BD%A0/foo',
dot: 'https://example.org/./a/../b/./c',
}
После недавнего апгрейда Ada 2.0 в Node.js 20 можно с уверенностью сказать, что при сравнении Node.js 18 с Node.js 20 произошли значительные улучшения:
А бенчмарк-файл довольно прост:
function useWHATWGWithoutBase(data) {
const len = data.length;
let result = new URL(data[0]); // Avoid dead code elimination
bench.start();
for (let i = 0; i < len; ++i) {
result = new URL(data[i]);
}
bench.end(len);
return result;
}
function useWHATWGWithBase(data) {
const len = data.length;
let result = new URL(data[0][0], data[0][1]); // Avoid dead code elimination
bench.start();
for (let i = 0; i < len; ++i) {
const item = data[i];
result = new URL(item[0], item[1]);
}
bench.end(len);
return result;
}
Единственное различие заключается во втором параметре, который используется в качестве базы при создании/парсинге URL. Стоит также отметить, что при передаче базы (withBase='true'), как правило, процесс идет быстрее, чем при обычном использовании (new URL(data)). Посмотрите все результаты в расширенном виде в основном репозитории.
Буферы
В Node.js буферы используются для работы с двоичными данными. Буферы — это встроенная структура данных, которая может использоваться для хранения необработанных двоичных данных в памяти, что может быть полезно при работе с сетевыми протоколами, операциями файловой системы или другими низкоуровневыми операциями. В целом, буферы являются важной составляющей Node.js и широко используются в платформе для работы с двоичными данными.
Для тех из вас, кто прямо или косвенно использует буферы Node.js, у меня есть хорошие новости (в основном для первопроходцев Node.js 20).
Помимо улучшения производительности Buffer.from()
Node.js 20 исправил две основные регрессии из Node.js 18:
Buffer.concat()
Node.js версии 20 продемонстрировал значительные улучшения по сравнению с версией 18, и эти улучшения остаются очевидными даже по сравнению с версией 16:
Buffer.toJSON()
С Node.js 16 до Node.js 18 наблюдалось падение производительности Buffer.toJSON на 88%:
$ node-benchmark-compare compare-buffers-16-18.csv
confidence improvement accuracy (*) (**) (***)
buffers/buffer-tojson.js len=256 n=10000 *** -81.12 % ±1.25% ±1.69% ±2.24%
buffers/buffer-tojson.js len=4096 n=10000 *** -88.39 % ±0.69% ±0.93% ±1.23%
Однако эта регрессия была исправлена и улучшена в Node.js 20 на порядки!
$ node-benchmark-compare compare-buffers-18-20.csv
confidence improvement accuracy (*) (**) (***)
buffers/buffer-tojson.js len=256 n=10000 *** 482.81 % ±7.02% ±9.42% ±12.42%
buffers/buffer-tojson.js len=4096 n=10000 *** 763.34 % ±5.22% ±7.04% ±9.34%
Поэтому правильно утверждать, что Node.js 20 — самая быстрая версия Node.js при работе с буферами.
Полное сравнение между Node.js 20 и Node.js 18 смотрите ниже:
$ node-benchmark-compare compare-buffers-18-20.csv
confidence improvement accuracy (*) (**) (***)
buffers/buffer-base64-decode.js size=8388608 n=32 *** 1.66 % ±0.10% ±0.14% ±0.18%
buffers/buffer-base64-encode.js n=32 len=67108864 *** -0.44 % ±0.17% ±0.23% ±0.30%
buffers/buffer-compare.js n=1000000 size=16 *** -3.14 % ±0.82% ±1.09% ±1.41%
buffers/buffer-compare.js n=1000000 size=16386 *** -15.56 % ±5.97% ±7.95% ±10.35%
buffers/buffer-compare.js n=1000000 size=4096 -2.63 % ±3.09% ±4.11% ±5.35%
buffers/buffer-compare.js n=1000000 size=512 *** -6.15 % ±1.28% ±1.71% ±2.24%
buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=1 pieces=16 *** 300.67 % ±0.71% ±0.95% ±1.24%
buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=1 pieces=4 *** 212.56 % ±4.81% ±6.47% ±8.58%
buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=16 pieces=16 *** 287.63 % ±2.47% ±3.32% ±4.40%
buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=16 pieces=4 *** 216.54 % ±1.24% ±1.66% ±2.17%
buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=256 pieces=16 *** 38.44 % ±1.04% ±1.38% ±1.80%
buffers/buffer-concat.js n=800000 withTotalLength=0 pieceSize=256 pieces=4 *** 91.52 % ±3.26% ±4.38% ±5.80%
buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=1 pieces=16 *** 192.63 % ±0.56% ±0.74% ±0.97%
buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=1 pieces=4 *** 157.80 % ±1.52% ±2.02% ±2.64%
buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=16 pieces=16 *** 188.71 % ±2.33% ±3.12% ±4.10%
buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=16 pieces=4 *** 151.18 % ±1.13% ±1.50% ±1.96%
buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=256 pieces=16 *** 20.83 % ±1.29% ±1.72% ±2.25%
buffers/buffer-concat.js n=800000 withTotalLength=1 pieceSize=256 pieces=4 *** 59.13 % ±3.18% ±4.28% ±5.65%
buffers/buffer-from.js n=800000 len=100 source='array' *** 3.91 % ±0.50% ±0.66% ±0.87%
buffers/buffer-from.js n=800000 len=100 source='arraybuffer-middle' *** 11.94 % ±0.65% ±0.86% ±1.13%
buffers/buffer-from.js n=800000 len=100 source='arraybuffer' *** 12.49 % ±0.77% ±1.03% ±1.36%
buffers/buffer-from.js n=800000 len=100 source='buffer' *** 7.46 % ±1.21% ±1.62% ±2.12%
buffers/buffer-from.js n=800000 len=100 source='object' *** 12.70 % ±0.84% ±1.12% ±1.47%
buffers/buffer-from.js n=800000 len=100 source='string-base64' *** 2.91 % ±1.40% ±1.88% ±2.46%
buffers/buffer-from.js n=800000 len=100 source='string-utf8' *** 12.97 % ±0.77% ±1.02% ±1.33%
buffers/buffer-from.js n=800000 len=100 source='string' *** 16.61 % ±0.71% ±0.95% ±1.25%
buffers/buffer-from.js n=800000 len=100 source='uint16array' *** 5.64 % ±0.84% ±1.13% ±1.48%
buffers/buffer-from.js n=800000 len=100 source='uint8array' *** 6.75 % ±0.95% ±1.28% ±1.68%
buffers/buffer-from.js n=800000 len=2048 source='array' 0.03 % ±0.33% ±0.43% ±0.56%
buffers/buffer-from.js n=800000 len=2048 source='arraybuffer-middle' *** 11.73 % ±0.55% ±0.74% ±0.96%
buffers/buffer-from.js n=800000 len=2048 source='arraybuffer' *** 12.85 % ±0.55% ±0.73% ±0.96%
buffers/buffer-from.js n=800000 len=2048 source='buffer' *** 7.66 % ±1.28% ±1.70% ±2.21%
buffers/buffer-from.js n=800000 len=2048 source='object' *** 11.96 % ±0.90% ±1.20% ±1.57%
buffers/buffer-from.js n=800000 len=2048 source='string-base64' *** 4.10 % ±0.46% ±0.61% ±0.79%
buffers/buffer-from.js n=800000 len=2048 source='string-utf8' *** -1.30 % ±0.71% ±0.96% ±1.27%
buffers/buffer-from.js n=800000 len=2048 source='string' *** -2.23 % ±0.93% ±1.25% ±1.64%
buffers/buffer-from.js n=800000 len=2048 source='uint16array' *** 6.89 % ±1.44% ±1.91% ±2.49%
buffers/buffer-from.js n=800000 len=2048 source='uint8array' *** 7.74 % ±1.36% ±1.81% ±2.37%
buffers/buffer-tojson.js len=0 n=10000 *** -11.63 % ±2.34% ±3.11% ±4.06%
buffers/buffer-tojson.js len=256 n=10000 *** 482.81 % ±7.02% ±9.42% ±12.42%
buffers/buffer-tojson.js len=4096 n=10000 *** 763.34 % ±5.22% ±7.04% ±9.34%
Кодирование и декодирование текста
TextDecoder и TextEncoder — это два класса JavaScript, которые являются частью спецификации Web API и доступны в современных веб-браузерах и Node.js. Вместе TextDecoder и TextEncoder обеспечивают простой и эффективный способ работы с текстовыми данными в JavaScript, позволяя разработчикам выполнять различные операции со строками и кодировками символов.
Декодирование и кодирование становится значительно быстрее, чем в Node.js 18. С добавлением simdutf для парсинга UTF-8 в наблюдаемом бенчмарке результаты улучшились на 364% (чрезвычайно впечатляющий скачок) при декодировании по сравнению с Node.js 16.
Эти достижения стали еще более заметными в Node.js 20: производительность выросла на 25% по сравнению с Node.js 18. Полные результаты можно посмотреть в репозитории state-of-nodejs-performance-2023.
Повышение производительности также наблюдалось при сравнении методов кодирования на Node.js 18. От Node.js 16 до Node.js 18, TextEncoder.encodeInto достиг 93,67% улучшения в текущем наблюдении (использование ascii с длиной строки 256):
Операции бенчмаркинга в Node.js
Операции бенчмаркинга в Node.js всегда вызывали у меня любопытство. Как человек, которому нравится изучать тонкости Node.js и лежащих в его основе технологий, я нахожу увлекательным углубляться в детали этих операций, особенно тех, которые связаны с движком V8. На самом деле, я часто люблю делиться своими открытиями с другими людьми в рамках выступлений и семинаров, проводимых компанией NearForm, с которой я связан. Если вам интересно, то можно найти больше информации о моих презентациях на данную тему, перейдя по этой ссылке.
Кроме того, в этих бенчмарках будет использоваться метрика ops/sec, которая в основном означает количество операций, выполненных за одну секунду. Важно подчеркнуть, что это может означать лишь очень малую часть вашего машинного времени. Если вы читали мою предыдущую статью (Подготовка и оценка бенчмарков), то должны помнить раздел "Оценка результатов", где я рассматриваю проблему ops/sec в реальных приложениях — если нет, то вам стоит вернуться к нему.
Парсинг целых чисел
Парсинг строк в числа может быть выполнен с помощью + или parseInt(x, 10). Результаты предыдущих бенчмарков показали, что использование + было быстрее, чем parseInt(x, 10) в ранних версиях Node.js, как показано в таблице ниже:
name |
ops/sec |
samples |
---|---|---|
Using parseInt(x, 10) - small number (2 len) |
283,768,532 |
91 |
Using parseInt(x, 10) - big number (10 len) |
21,307,115 |
100 |
Using + - small number (2 len) |
849,906,952 |
100 |
Using + - big number (10 len) |
849,173,336 |
97 |
Однако с выходом Node.js 20 и новой версии V8 (11.4) обе операции по производительности оказались эквивалентными, как показано в обновленных результатах бенчмарков ниже:
name |
ops/sec |
samples |
---|---|---|
Using parseInt(x, 10) - small number (2 len) |
856,413,575 |
98 |
Using parseInt(x, 10) - big number (10 len) |
856,754,259 |
96 |
Using + - small number (2 len) |
857,364,191 |
98 |
Using + - big number (10 len) |
857,511,971 |
96 |
Super в сравнении с This
Одним из интересных бенчмарков, изменившихся с добавлением Node.js 20, является использование this или super в классах, как видно из приведенного ниже примера:
class Base {
foo () {
return 10 * 1e2
}
}
class SuperClass extends Base {
bar () {
const tmp = 20 * 23
return super.foo() + tmp
}
}
class ThisClass extends Base {
bar () {
const tmp = 20 * 23
return this.foo() + tmp
}
}
Сравнение между super и this в Node.js 18 давало следующие операции в секунду (ops/sec):
name |
ops/sec |
samples |
---|---|---|
Using super |
159,426,608 |
96 |
Using this |
160,092,440 |
91 |
Нет существенной разницы между обоими подходами и на Node.js 20. Это утверждение справедливо с небольшим различием:
name |
ops/sec |
samples |
---|---|---|
Using super |
850,760,436 |
97 |
Using this |
853,619,840 |
99 |
По результатам бенчмарков видно, что производительность при использовании this на Node.js 20 значительно увеличилась по сравнению с Node.js 18. Этот прирост весьма заметен: поскольку this на Node.js 20 достигает впечатляющих 853 619 840 оп/с по сравнению с 160 092 440 оп/с на Node.js 18, что на 433% лучше! По-видимому, у него такой же метод доступа к свойствам, как и у обычного объекта: obj.property1
. Также отметим, что обе операции были протестированы в одном и том же выделенном окружении. Следовательно, это вряд ли произошло случайно.
Доступ к свойствам
В JavaScript существуют различные способы добавления свойств к объектам, каждый из которых имеет свое собственное назначение и иногда неоднозначен по своей природе. Как разработчик, вы можете заинтересоваться эффективностью доступа к свойствам в каждом из этих методов.
Хорошей новостью является то, что репозиторий nodejs-bench-operations включает сравнение этих методов, которое проливает свет на их характеристики производительности. На самом деле, данные бенчмарка показывают, что доступ к свойствам в Node.js 20 значительно улучшился, особенно при использовании объектов со свойствами writable: true
и enumerable/configurable: false
.
const myObj = {};
Object.defineProperty(myObj, 'test', {
writable: true,
value: 'Hello',
enumerable: false,
configurable: false,
});
myObj.test // How fast is the property access?
На Node.js 18 доступ к свойству (myObj.test) продуцировал 166 422 265 оп/с. Однако при тех же обстоятельствах Node.js 20 производит 857 316 403 оп/с! Эту и другие особенности доступа к свойствам можно увидеть в следующих результатах бенчмарка:
Array.prototype.at
Array.prototype.at(-1) — это метод, который был введен в спецификации ECMAScript 2021. Он позволяет получить доступ к последнему элементу массива, не зная его длины и не используя отрицательные индексы, что может быть полезным в некоторых юзкейсах. Таким образом, метод at() обеспечивает более лаконичный и читабельный способ доступа к последнему элементу массива по сравнению с традиционными методами, такими как array[array.length - 1]
.
На Node.js 18 этот доступ был значительно медленнее по сравнению с Array[length-1]
:
name |
ops/sec |
samples |
---|---|---|
Length = 100 - Array.at |
26,652,680 |
99 |
Length = 10_000 - Array.at |
26,317,564 |
97 |
Length = 1_000_000 - Array.at |
27,187,821 |
98 |
Length = 100 - Array[length - 1] |
848,118,011 |
98 |
Length = 10_000 - Array[length - 1] |
847,958,319 |
100 |
Length = 1_000_000 - Array[length - 1] |
847,796,498 |
101 |
Начиная с Node.js 19, Array.prototype.at эквивалентен старомодному Array[length-1], как показано в таблице ниже:
name |
ops/sec |
samples |
---|---|---|
Length = 100 - Array.at |
852,980,778 |
99 |
Length = 10_000 - Array.at |
854,299,272 |
99 |
Length = 1_000_000 - Array.at |
853,374,694 |
98 |
Length = 100 - Array[length - 1] |
854,589,197 |
95 |
Length = 10_000 - Array[length - 1] |
856,122,244 |
95 |
Length = 1_000_000 - Array[length - 1] |
856,557,974 |
99 |
String.prototype.includes
Большинство людей знают, что RegExp зачастую является источником многих узких мест в любом приложении. Например, вы захотите проверить, содержит ли определенная переменная application/json. И хотя вы можете сделать это несколькими способами, чаще всего используется либо:
/application/json/.test(text)
— RegEx
или
text.includes('application/json')
— String.prototype.includes
Некоторые из вас могут не знать, что String.prototype.includes
практически такой же медленный, как RegExp на Node.js 16.
name |
ops/sec |
samples |
---|---|---|
Using includes |
16,056,204 |
97 |
Using indexof |
850,710,330 |
100 |
Using RegExp.test |
15,227,370 |
98 |
Using RegExp.text with cached regex pattern |
15,808,350 |
97 |
Using new RegExp.test |
4,945,475 |
98 |
Using new RegExp.test with cached regex pattern |
5,944,679 |
100 |
Однако, начиная с Node.js 18, такое поведение было исправлено.
name |
ops/sec |
samples |
---|---|---|
Using includes |
856,127,951 |
101 |
Using indexof |
856,709,023 |
98 |
Using RegExp.test |
16,623,756 |
98 |
Using RegExp.text with cached regex pattern |
16,952,701 |
99 |
Using new RegExp.test |
4,704,351 |
95 |
Using new RegExp.test with cached regex pattern |
5,660,755 |
95 |
Crypto.verify
В Node.js модуль crypto предоставляет набор криптографических функций, которые могут быть использованы для различных целей, таких как создание и проверка цифровых подписей, шифрование и расшифровка данных, а также генерация безопасных случайных чисел. Одним из методов, доступных в этом модуле, является crypto.verify()
, который используется для проверки цифровой подписи, созданной методом crypto.sign()
.
Node.js 14 (*Окончание эксплуатации) использует OpenSSL 1.x. В Node.js 16 мы добавили протокол QUIC, но по-прежнему применяем OpenSSL версии 1. Однако в Node.js 18 мы обновили OpenSSL до версии 3.x (поверх QUIC), и после Node.js 18 была обнаружена регрессия, которая снизила скорость с 30 тыс. оп/с до 6~7 тыс. оп/с. Как я уже упоминал в твите, весьма вероятно, что это вызвано новой версией OpenSSL. Опять же, наша команда изучает данную проблему, и если у вас есть какие-либо соображения по этому поводу, не стесняйтесь ее комментировать: https://github.com/nodejs/performance/issues/72.
Инициативы по повышению производительности Node.js
Команда Node.js всегда тщательно следила за тем, чтобы ее API и основные функциональные возможности были оптимизированы для повышения скорости и использования ресурсов.
С целью дальнейшего повышения производительности Node.js команда недавно представила новую стратегическую инициативу под названием "Performance" ("Производительность"), которую возглавляет Ягиз Низипли (Yagiz Nizipli). Она направлена на выявление и устранение узких мест в рантайме Node.js и основных модулей, а также на повышение общей производительности и масштабируемости платформы.
Помимо инициативы Performance, в настоящее время реализуется еще несколько проектов, направленных на оптимизацию различных аспектов Node.js. Одной из них является инициатива 'Startup Snapshot', которую возглавляет Джойи Чунг (Joyee Cheung). Она направлена на снижение времени запуска приложений Node.js, что является критическим фактором для повышения общей производительности и удобства работы веб-приложений.
Поэтому, если вам интересна эта тема, подумайте о том, чтобы присоединиться к встречам каждые две недели, и не стесняйтесь отправлять сообщения в канале #nodejs-core-performance на OpenJS Foundation Slack.
То, за чем нужно следить
Помимо стратегических инициатив, существует несколько пулл-реквестов (PR), которые с большой вероятностью окажут существенное влияние на производительность Node.js — в данный момент я пишу нижеследующий пост (он еще не смерджен):
Ошибки Node.js — https://github.com/nodejs/node/pull/46648
Возникающие погрешности в Node.js обходятся очень дорого. Это нередко является источником критических моментов в приложениях Node.js. В качестве примера я провел исследование имплементации fetch в Node.js (undici) и обнаружил, что одной из зол в реализации Node.js WebStreams является процесс создания ошибок. Следовательно, проведя оптимизацию ошибочных объектов в Node.js, мы можем повысить общую эффективность платформы и снизить риск возникновения узких мест.
Сжатие указателей — https://github.com/nodejs/build/issues/3204
Сжатие указателей — это техника, используемая в компьютерном программировании для уменьшения использования памяти программами, которые используют много указателей. Хотя это не улучшает производительность напрямую, но может косвенно ее повысить за счет уменьшения обращений к кэшу и ошибок страниц. Это, безусловно, может снизить некоторые затраты на инфраструктуру, как описано в данной теме.
Увеличить дефолтный
--max-semi-space-size -
https://github.com/nodejs/node/pull/47277
В марте 2022 года был поднят вопрос, предлагающий увеличить V8 max_semi_space_size
с целью уменьшить количество операций по сборке мусора (в частности, Scavenge) и увеличить общую пропускную способность в бенчмарке веб-инструментария. Мы пока оцениваем его влияние, и он может попасть или нет в Node.js 21.
увеличить значение
highWaterMark
для потоков Node.js Readable/Writable - https://github.com/nodejs/node/pull/46608
Этот PR увеличивает значение по умолчанию для highWaterMark
в потоках Node.js. Ожидается улучшение производительности при использовании потоков Node.js с параметрами по умолчанию. Однако данный PR является semver-major изменением и должен появиться в Node.js 21. Для получения подробных результатов бенчмарка, ждите: 'State of Node.js Performance 2023 — P2' в конце года.
Заключение
Несмотря на некоторые регрессии в потоках Node.js и криптомодуле, Node.js 20 может похвастаться значительным улучшением производительности по сравнению с предыдущими версиями. Заметные улучшения наблюдаются в таких операциях JavaScript, как доступ к свойствам, парсинг URL, кодирование и декодирование буферов/текста, время жизненного цикла запуска/процесса и EventTarget, среди прочих.
Команда разработчиков производительности Node.js (nodejs/performance) расширила сферу своей деятельности, что приводит к увеличению вклада в оптимизацию производительности с каждой новой версией. Эта тенденция указывает на то, что Node.js продолжит становиться быстрее с течением времени.
Стоит отметить, что бенчмарки фокусируются на определенных операциях, которые могут напрямую влиять или нет на ваш конкретный юзкейс. Поэтому я настоятельно рекомендую просмотреть все результаты бенчмарков в репозитории state-of-nodejs-performance и убедиться, что данные операции соответствуют вашим бизнес-требованиям.
В заключение материала рекомендуем два открытых урока, которые пройдут в ближайшие дни в рамках онлайн-курсов OTUS:
Построение Rest API с Express: займемся добавлением общих стандартов создания API; рассмотрим REST, принципы построения API; проанализируем технологии аутентификации в веб приложениях. Записаться на урок
Основы HTML: создадим разметку страницы авторизации — разберём основы семантики и атрибуты полей формы. Разберем типичные ошибки. Записаться на урок