Пришло время использовать Node 8?
Опубликовано: 2022-03-11Узел 8 вышел! На самом деле, Node 8 существует уже достаточно долго, чтобы его можно было использовать в реальных условиях. Он поставляется с новым быстрым движком V8 и новыми функциями, включая async/await, HTTP/2 и асинхронные перехватчики. Но готов ли он для вашего проекта? Давайте узнаем!
Примечание редактора: вы, вероятно, знаете, что Node 10 (кодовое название Dubnium ) тоже отсутствует. Мы решили сосредоточиться на Node 8 ( Carbon ) по двум причинам: (1) Node 10 только сейчас вступает в фазу долгосрочной поддержки (LTS) и (2) Node 8 отметил более значительную итерацию, чем Node 10. .
Производительность в Node 8 LTS
Мы начнем с обзора улучшений производительности и новых функций этого замечательного релиза. Одной из основных областей улучшения является движок Node JavaScript.
Что такое движок JavaScript?
Движок JavaScript выполняет и оптимизирует код. Это может быть стандартный интерпретатор или JIT-компилятор, который компилирует JavaScript в байт-код. Движки JS, используемые Node.js, являются JIT-компиляторами, а не интерпретаторами.
Двигатель V8
Node.js с самого начала использовал движок Google Chrome V8 JavaScript , или просто V8 . Некоторые выпуски Node используются для синхронизации с более новой версией V8. Но будьте осторожны, чтобы не спутать V8 с Node 8, поскольку мы сравниваем версии V8 здесь.
Об этом легко споткнуться, поскольку в контексте программного обеспечения мы часто используем «v8» в качестве сленга или даже официальную краткую форму для «версии 8», поэтому некоторые могут смешивать «Node V8» или «Node.js V8» с «NodeJS 8». », но мы избегали этого на протяжении всей статьи, чтобы внести ясность: V8 всегда будет означать движок, а не версию Node.
V8 выпуск 5
Node 6 использует пятую версию V8 в качестве движка JavaScript. (Первые несколько точечных выпусков Node 8 также используют V8, выпуск 5, но они используют более новый точечный выпуск V8, чем Node 6.)
Компиляторы
Выпуски V8 5 и более ранние имеют два компилятора:
- Full-codegen — это простой и быстрый JIT-компилятор, но он производит медленный машинный код.
- Crankshaft — это сложный JIT-компилятор, создающий оптимизированный машинный код.
Потоки
В глубине души V8 использует несколько типов потоков:
- Основной поток получает код, компилирует его, а затем выполняет.
- Вторичные потоки выполняют код, в то время как основной поток оптимизирует код.
- Поток профилировщика информирует среду выполнения о неэффективных методах. Затем Crankshaft оптимизирует эти методы.
- Другие потоки управляют сборкой мусора.
Процесс компиляции
Сначала компилятор Full-codegen выполняет код JavaScript. Во время выполнения кода поток профилировщика собирает данные, чтобы определить, какие методы будет оптимизировать движок. В другом потоке Crankshaft оптимизирует эти методы.
Проблемы
У упомянутого выше подхода есть две основные проблемы. Во-первых, он архитектурно сложен. Во-вторых, скомпилированный машинный код потребляет гораздо больше памяти. Объем потребляемой памяти не зависит от количества выполнений кода. Даже код, который запускается только один раз, также занимает значительный объем памяти.
V8 выпуск 6
Первой версией Node, в которой используется движок V8 Release 6, является Node 8.3.
В выпуске 6 команда V8 создала Ignition и TurboFan, чтобы смягчить эти проблемы. Ignition и TurboFan заменяют Full-codegen и CrankShaft соответственно.
Новая архитектура более проста и потребляет меньше памяти.
Ignition компилирует код JavaScript в байт-код вместо машинного кода, экономя много памяти. После этого оптимизирующий компилятор TurboFan генерирует из этого байт-кода оптимизированный машинный код.
Конкретные улучшения производительности
Давайте рассмотрим области, в которых производительность Node 8.3+ изменилась по сравнению со старыми версиями Node.
Создание объектов
Создание объектов примерно в пять раз быстрее в Node 8.3+, чем в Node 6.
Функция Размер
Двигатель V8 решает, следует ли оптимизировать функцию, основываясь на нескольких факторах. Одним из факторов является размер функции. Маленькие функции оптимизируются, а длинные — нет.
Как рассчитывается размер функции?
Коленчатый вал в старом двигателе V8 использует «счетчик символов» для определения размера функции. Пробелы и комментарии в функции снижают вероятность ее оптимизации. Я знаю, это может вас удивить, но тогда комментарий мог снизить скорость примерно на 10%.
В Node 8.3+ ненужные символы, такие как пробелы и комментарии, не влияют на производительность функции. Почему бы нет?
Потому что новый TurboFan не считает символы для определения размера функции. Вместо этого он подсчитывает узлы абстрактного синтаксического дерева (AST), поэтому эффективно учитывает только фактические инструкции функций . Используя Node 8.3+, вы можете добавлять сколько угодно комментариев и пробелов.
Аргументы, определяющие Array
Обычные функции в JavaScript несут неявный Array
-подобный объект argument
.
Что означает Array
?
Объект arguments
действует как массив. У него есть свойство length
, но отсутствуют встроенные методы Array
, такие как forEach
и map
.
Вот как работает объект arguments
:
function foo() { console.log(arguments[0]); // Expected output: a console.log(arguments[1]); // Expected output: b console.log(arguments[2]); // Expected output: c } foo("a", "b", "c");
Итак, как мы можем преобразовать объект arguments
в массив? Используя краткий Array.prototype.slice.call(arguments)
.
function test() { const r = Array.prototype.slice.call(arguments); console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output: [2, 4, 6]
Array.prototype.slice.call(arguments)
снижает производительность во всех версиях Node. Поэтому копирование ключей через цикл for
работает лучше:
function test() { const r = []; for (index in arguments) { r.push(arguments[index]); } console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]
Цикл for
немного громоздкий, не так ли? Мы могли бы использовать оператор распространения, но он работает медленно в Node 8.2 и ниже:
function test() { const r = [...arguments]; console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]
Ситуация изменилась в Node 8.3+. Теперь распространение выполняется намного быстрее, даже быстрее, чем цикл for.
Частичное применение (каррирование) и привязка
Каррирование — это разбиение функции, которая принимает несколько аргументов, на серию функций, где каждая новая функция принимает только один аргумент.
Допустим, у нас есть простая функция add
. Каррированная версия этой функции принимает один аргумент, num1
. Он возвращает функцию, которая принимает еще один аргумент num2
и возвращает сумму num1
и num2
:
function add(num1, num2) { return num1 + num2; } add(4, 6); // returns 10 function curriedAdd(num1) { return function(num2) { return num1 + num2; }; } const add5 = curriedAdd(5); add5(3); // returns 8
Метод bind
возвращает каррированную функцию с более кратким синтаксисом.
function add(num1, num2) { return num1 + num2; } const add5 = add.bind(null, 5); add5(3); // returns 8
Так что bind
невероятна, но в старых версиях Node работает медленно. В Node 8.3+ bind
работает намного быстрее, и вы можете использовать ее, не беспокоясь о снижении производительности.
Эксперименты
Было проведено несколько экспериментов для сравнения производительности Node 6 и Node 8 на высоком уровне. Обратите внимание, что они были проведены на Node 8.0, поэтому они не включают упомянутые выше улучшения, характерные для Node 8.3+ благодаря обновлению V8 до версии 6.
Время серверного рендеринга в Node 8 было на 25% меньше, чем в Node 6. В крупных проектах количество экземпляров сервера можно было сократить со 100 до 75. Это поразительно. Тестирование набора из 500 тестов в Node 8 было на 10 % быстрее. Сборки Webpack были на 7% быстрее. В целом результаты показали заметный прирост производительности в Node 8.
Особенности узла 8
Скорость была не единственным улучшением в Node 8. Он также принес несколько удобных новых функций — возможно, самая важная — async/await .
Асинхронное/ожидание в узле 8
Обратные вызовы и обещания обычно используются для обработки асинхронного кода в JavaScript. Обратные вызовы печально известны созданием неподдерживаемого кода. Они вызвали хаос (известный как ад обратных вызовов ) в сообществе JavaScript. Промисы долгое время спасали нас от ада обратных вызовов, но им все еще не хватало чистоты синхронного кода. Async/await — это современный подход, позволяющий писать асинхронный код, который выглядит как синхронный код.
И хотя async/await можно было использовать в предыдущих версиях Node, для этого требовались внешние библиотеки и инструменты — например, дополнительная предварительная обработка с помощью Babel. Теперь он доступен изначально, из коробки.
Я расскажу о некоторых случаях, когда async/await превосходит обычные промисы.
Условные
Представьте, что вы извлекаете данные и определяете, нужен ли новый вызов API, исходя из полезной нагрузки . Взгляните на приведенный ниже код, чтобы увидеть, как это делается с помощью подхода «обычных промисов».
const request = () => { return getData().then(data => { if (!data.car) { return fetchForCar(data.id).then(carData => { console.log(carData); return carData; }); } else { console.log(data); return data; } }); };
Как видите, приведенный выше код уже выглядит беспорядочно, только из-за одного лишнего условия. Async/await требует меньше вложенности:
const request = async () => { const data = await getData(); if (!data.car) { const carData = await fetchForCar(data); console.log(carData); return carData; } else { console.log(data); return data; } };
Обработка ошибок
Async/await предоставляет вам доступ для обработки как синхронных, так и асинхронных ошибок в try/catch. Допустим, вы хотите проанализировать JSON, поступающий из асинхронного вызова API. Одна операция try/catch может обрабатывать как ошибки синтаксического анализа, так и ошибки API.
const request = async () => { try { console.log(await getData()); } catch (err) { console.log(err); } };
Промежуточные значения
Что, если промису нужен аргумент, который должен быть разрешен из другого промиса? Это означает, что асинхронные вызовы должны выполняться последовательно.
Используя обычные промисы, вы можете получить такой код:
const request = () => { return fetchUserData() .then(userData => { return fetchCompanyData(userData); }) .then(companyData => { return fetchRetiringPlan(userData, companyData); }) .then(retiringPlan => { const retiringPlan = retiringPlan; }); };
Async/await блестит в этом случае, когда необходимы связанные асинхронные вызовы:
const request = async () => { const userData = await fetchUserData(); const companyData = await fetchCompanyData(userData); const retiringPlan = await fetchRetiringPlan(userData, companyData); };
Асинхронно параллельно
Что делать, если вы хотите вызвать более одной асинхронной функции параллельно? В приведенном ниже коде мы дождемся разрешения fetchHouseData
, а затем вызовем fetchCarData
. Хотя каждый из них независим от другого, они обрабатываются последовательно. Вы будете ждать две секунды для разрешения обоих API. Это не хорошо.

function fetchHouseData() { return new Promise(resolve => setTimeout(() => resolve("Mansion"), 1000)); } function fetchCarData() { return new Promise(resolve => setTimeout(() => resolve("Ferrari"), 1000)); } async function action() { const house = await fetchHouseData(); // Wait one second const car = await fetchCarData(); // ...then wait another second. console.log(house, car, " in series"); } action();
Лучшим подходом является параллельная обработка асинхронных вызовов. Проверьте приведенный ниже код, чтобы получить представление о том, как это достигается в async/await.
async function parallel() { houseDataPromise = fetchHouseData(); carDataPromise = fetchCarData(); const house = await houseDataPromise; // Wait one second for both const car = await carDataPromise; console.log(house, car, " in parallel"); } parallel();
Параллельная обработка этих вызовов требует ожидания только одной секунды для обоих вызовов.
Новые функции основной библиотеки
Node 8 также предлагает несколько новых основных функций.
Копировать файлы
До Node 8 для копирования файлов мы создавали два потока и передавали данные из одного в другой. В приведенном ниже коде показано, как поток чтения передает данные потоку записи. Как видите, для такого простого действия, как копирование файла, код загроможден.
const fs = require('fs'); const rd = fs.createReadStream('sourceFile.txt'); rd.on('error', err => { console.log(err); }); const wr = fs.createWriteStream('target.txt'); wr.on('error', err => { console.log(err); }); wr.on('close', function(ex) { console.log('File Copied'); }); rd.pipe(wr);
В Node 8 fs.copyFile
и fs.copyFileSync
— это новые подходы к копированию файлов с гораздо меньшими трудностями.
const fs = require("fs"); fs.copyFile("firstFile.txt", "secondFile.txt", err => { if (err) { console.log(err); } else { console.log("File copied"); } });
Обещание и обратный вызов
util.promisify
преобразует обычную функцию в асинхронную. Обратите внимание, что введенная функция должна соответствовать общему стилю обратного вызова Node.js. Он должен принимать обратный вызов в качестве последнего аргумента, т. е. (error, payload) => { ... }
.
const { promisify } = require('util'); const fs = require('fs'); const readFilePromisified = promisify(fs.readFile); const file_path = process.argv[2]; readFilePromisified(file_path) .then((text) => console.log(text)) .catch((err) => console.log(err));
Как видите, util.promisify
преобразовал fs.readFile
в асинхронную функцию.
С другой стороны, Node.js поставляется с util.callbackify
. util.callbackify
является противоположностью util.promisify
: он преобразует асинхронную функцию в функцию обратного вызова Node.js.
destroy
функцию для чтения и записи
Функция destroy
в Node 8 — это задокументированный способ уничтожить/закрыть/прервать доступный для чтения или записи поток:
const fs = require('fs'); const file = fs.createWriteStream('./big.txt'); file.on('error', errors => { console.log(errors); }); file.write(`New text.\n`); file.destroy(['First Error', 'Second Error']);
Приведенный выше код приводит к созданию нового файла с именем big.txt
(если он еще не существует) с текстом New text.
.
Функции Readable.destroy
и Writeable.destroy
в Node 8 генерируют событие close
и необязательное событие error
— destroy
не обязательно означает, что что-то пошло не так.
Оператор спреда
Оператор распространения (также известный как ...
) работал в Node 6, но только с массивами и другими итерируемыми объектами:
const arr1 = [1,2,3,4,5,6] const arr2 = [...arr1, 9] console.log(arr2) // expected output: [1,2,3,4,5,6,9]
В Node 8 объекты также могут использовать оператор распространения:
const userCarData = { type: 'ferrari', color: 'red' }; const userSettingsData = { lastLoggedIn: '12/03/2019', featuresPlan: 'premium' }; const userData = { ...userCarData, name: 'Youssef', ...userSettingsData }; console.log(userData); /* Expected output: { type: 'ferrari', color: 'red', name: 'Youssef', lastLoggedIn: '12/03/2019', featuresPlan: 'premium' } */
Экспериментальные функции в Node 8 LTS
Экспериментальные функции нестабильны, могут устареть и со временем обновляться. Не используйте какие-либо из этих функций в рабочей среде , пока они не станут стабильными.
Асинхронные хуки
Асинхронные хуки отслеживают время жизни асинхронных ресурсов, созданных внутри Node через API.
Убедитесь, что вы понимаете цикл обработки событий, прежде чем переходить к асинхронным перехватчикам. Это видео может помочь. Асинхронные хуки полезны для отладки асинхронных функций. У них есть несколько приложений; один из них — трассировка стека ошибок для асинхронных функций.
Взгляните на код ниже. Обратите внимание, что console.log
— это асинхронная функция. Таким образом, его нельзя использовать внутри асинхронных хуков. Вместо этого используется fs.writeSync
.
const asyncHooks = require('async_hooks'); const fs = require('fs'); const init = (asyncId, type, triggerId) => fs.writeSync(1, `${type} \n`); const asyncHook = asyncHooks.createHook({ init }); asyncHook.enable();
Посмотрите это видео, чтобы узнать больше об асинхронных хуках. В частности, с точки зрения руководства по Node.js, эта статья помогает демистифицировать асинхронные хуки с помощью иллюстративного приложения.
Модули ES6 в узле 8
Node 8 теперь поддерживает модули ES6, что позволяет использовать следующий синтаксис:
import { UtilityService } from './utility_service';
Чтобы использовать модули ES6 в Node 8, вам нужно сделать следующее.
- Добавьте флаг
--experimental-modules
в командную строку - Переименуйте расширения файлов с
.js
на.mjs
HTTP/2
HTTP/2 — это последнее обновление редко обновляемого протокола HTTP, и Node 8.4+ изначально поддерживает его в экспериментальном режиме. Он быстрее, надежнее и эффективнее своего предшественника HTTP/1.1. И Google рекомендует вам использовать его. Но что еще он делает?
Мультиплексирование
В HTTP/1.1 сервер мог отправлять только один ответ на соединение за раз. В HTTP/2 сервер может отправлять более одного ответа параллельно.
Сервер Push
Сервер может отправлять несколько ответов на один запрос клиента. Почему это выгодно? Возьмем в качестве примера веб-приложение. Условно,
- Клиент запрашивает HTML-документ.
- Клиент обнаруживает необходимые ресурсы из HTML-документа.
- Клиент отправляет HTTP-запрос для каждого требуемого ресурса. Например, клиент отправляет HTTP-запрос для каждого ресурса JS и CSS, упомянутого в документе.
Функция server-push использует тот факт, что сервер уже знает обо всех этих ресурсах. Сервер отправляет эти ресурсы клиенту. Итак, в примере с веб-приложением сервер отправляет все ресурсы после того, как клиент запрашивает исходный документ. Это уменьшает задержку.
Приоритизация
Клиент может установить схему приоритетов, чтобы определить, насколько важен каждый требуемый ответ. Затем сервер может использовать эту схему для определения приоритетов выделения памяти, ЦП, полосы пропускания и других ресурсов.
Избавьтесь от старых вредных привычек
Поскольку HTTP/1.1 не допускал мультиплексирования, используется несколько оптимизаций и обходных путей, чтобы скрыть медленную скорость и загрузку файлов. К сожалению, эти методы вызывают увеличение потребления оперативной памяти и задержку рендеринга:
- Разделение домена: использовалось несколько поддоменов, чтобы соединения были рассредоточены и обрабатывались параллельно.
- Объединение файлов CSS и JavaScript для уменьшения количества запросов.
- Карты спрайтов: объединение файлов изображений для уменьшения HTTP-запросов.
- Встраивание: CSS и JavaScript размещаются непосредственно в HTML, чтобы уменьшить количество подключений.
Теперь с HTTP/2 вы можете забыть об этих методах и сосредоточиться на своем коде.
Но как вы используете HTTP/2?
Большинство браузеров поддерживают HTTP/2 только через защищенное соединение SSL. Эта статья поможет вам настроить самозаверяющий сертификат. Добавьте сгенерированный файл .crt
и файл .key
в каталог с именем ssl
. Затем добавьте приведенный ниже код в файл с именем server.js
.
Не забудьте использовать флаг --expose-http2
в командной строке, чтобы включить эту функцию. Т.е. команда запуска для нашего примера — node server.js --expose-http2
.
const http2 = require('http2'); const path = require('path'); const fs = require('fs'); const PORT = 3000; const secureServerOptions = { cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')), key: fs.readFileSync(path.join(__dirname, './ssl/server.key')) }; const server = http2.createSecureServer(secureServerOptions, (req, res) => { res.statusCode = 200; res.end('Hello from Toptal'); }); server.listen( PORT, err => err ? console.error(err) : console.log(`Server listening to port ${PORT}`) );
Конечно, Node 8, Node 9, Node 10 и т. д. по-прежнему поддерживают старый HTTP 1.1 — официальная документация Node.js по стандартной HTTP-транзакции еще долго не устареет. Но если вы хотите использовать HTTP/2, вы можете углубиться в это руководство по Node.js.
Итак, стоит ли мне использовать Node.js 8 в конце?
Node 8 появился с улучшениями производительности и новыми функциями, такими как async/await, HTTP/2 и другими. Сквозные эксперименты показали, что Node 8 примерно на 25% быстрее, чем Node 6. Это приводит к существенной экономии средств. Так что для новых проектов, абсолютно! Но для существующих проектов следует ли обновлять Node?
Это зависит от того, потребуется ли вам изменить большую часть существующего кода. В этом документе перечислены все критические изменения Node 8, если вы переходите с Node 6. Чтобы избежать распространенных проблем, переустановите все пакеты npm
вашего проекта, используя последнюю версию Node 8. Кроме того, всегда используйте ту же версию Node.js на компьютерах для разработки, что и на рабочих серверах. Удачи!
- На кой черт мне использовать Node.js? Индивидуальное руководство
- Отладка утечек памяти в приложениях Node.js
- Создание безопасного REST API в Node.js
- Cabin Fever Coding: Учебное пособие по серверной части Node.js