[Из песочницы] Правильное использование require в node.js
Предисловие
Не так давно проект, на котором я работаю в данный момент, начал использовать модульную систему ES2015. Я не буду заострять внимание на этой технологии JavaScript, т.к статья совсем не об этом, а о том как технология сподвигла меня к одной мысли.
Как многие знают, ES2015 Modules представляют собой импортирование/экспортирование скриптов крайне схожее по синтаксису с python
и многими другими языками программирования. Пример:
// Helper.js
export function includes(array, variable) {
return array.indexOf(variable) !== -1;
}
// main.js
import {includes} from 'Helper';
assets(includes([1,2,3], 2), true);
Все, кто интересовался модулями JavaScript знают, что импортирование и экспортирование возможно только на верхнем уровне модуля (файла с кодом).
Следующий грубый пример кода вызовет ошибки:
// sendEmail.js
export default function sendEmails(emails_list) {
import sender from 'sender';
export sender;
// сделать что-то
}
Exception: SyntaxError: import/export declarations may only appear at top level of a module
В отличие от ES2015 Modules — в модульной системе node.js
импортирование и экспортирование возможны на любом уровне вложенности.
Аналогичный код на node.js
не вызовет ошибку:
// sendEmail.js
module.exports = function sendEmails(emails_list) {
const sender = require('sender');
exports.sender = sender;
// сделать что-то
}
Преимущество такого способа в том, что модули необходимые в обработчике явно импортированы внутри и не засоряют пространство имен модуля (особенно актуально, если импортируемый модуль нужен только в одном обработчике). Так же появляется возможность отложенного экспортирования данных модуля.
Основные минусы:
- Об отсутствии модуля вы узнаете только во время вызова соответствующего обработчика
- Путь к импортироемому модулю может измениться, что приведет к изменению в каждом месте импортирования (например, в вашем модуле, в различных обработчиках используется
lodash/object/defaults
и вы решили обновиться до 4.x версии, где подключать нужноlodash/defaults
).
Разбор полетов
В большинстве задач для которых используется node.js
— front-end или основной веб-сервер, и высокая нагрузка на node.js
частое явление. Пропуская способность вашего сервера должны быть максимально возможная.
Измерение пропускной способности
Для измерения пропускной способности веб-сервера используется великолепная утилита от Apache — ab. Если вы еще с ней не знакомы, то настоятельно рекомендую это сделать.
Код веб-сервера одинаков за исключением обработчиков.
Тест запускался на node.js 6.0
с использованием модуля ifnode
, сделанного на базе express
Импортирование модулей непосредственно в обработчик
Код:
const app = require('ifnode')();
const RequireTestingController = app.Controller({
root: '/',
map: {
'GET /not_imported': 'notImportedAction'
}
});
RequireTestingController.notImportedAction = function(request, response, next) {
const data = {
message: 'test internal and external require'
};
const _defaults = require('lodash/object/defaults');
const _assign = require('lodash/object/assign');
const _clone = require('lodash/lang/clone');
response.ok({
_defaults: _defaults(data, {
lodash: 'defaults'
}),
_assign: _assign(data, {
lodash: 'assign'
}),
_clone: _clone(data)
});
};
Результат:
$ ab -n 15000 -c 30 -q "http://localhost:8080/not_imported"
Server Hostname: localhost
Server Port: 8080
Document Path: /not_imported
Document Length: 233 bytes
Concurrency Level: 30
Time taken for tests: 4.006 seconds
Complete requests: 15000
Failed requests: 0
Total transferred: 6195000 bytes
HTML transferred: 3495000 bytes
Requests per second: 3744.32 [#/sec] (mean)
Time per request: 8.012 [ms] (mean)
Time per request: 0.267 [ms] (mean, across all concurrent requests)
Transfer rate: 1510.16 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 6
66% 7
75% 8
80% 8
90% 10
95% 15
98% 17
99% 20
100% 289 (longest request)
Импортирование модулей вначале файла
Код:
const app = require('ifnode')();
const _defaults = require('lodash/object/defaults');
const _assign = require('lodash/object/assign');
const _clone = require('lodash/lang/clone');
const RequireTestingController = app.Controller({
root: '/',
map: {
'GET /already_imported': 'alreadyImportedAction'
}
});
RequireTestingController.alreadyImportedAction = function(request, response, next) {
const data = {
message: 'test internal and external require'
};
response.ok({
_defaults: _defaults(data, {
lodash: 'defaults'
}),
_assign: _assign(data, {
lodash: 'assign'
}),
_clone: _clone(data)
});
};
Результат:
$ ab -n 15000 -c 30 -q "http://localhost:8080/already_imported"
Server Hostname: localhost
Server Port: 8080
Document Path: /already_imported
Document Length: 233 bytes
Concurrency Level: 30
Time taken for tests: 3.241 seconds
Complete requests: 15000
Failed requests: 0
Total transferred: 6195000 bytes
HTML transferred: 3495000 bytes
Requests per second: 4628.64 [#/sec] (mean)
Time per request: 6.481 [ms] (mean)
Time per request: 0.216 [ms] (mean, across all concurrent requests)
Transfer rate: 1866.83 [Kbytes/sec] received
Percentage of the requests served within a certain time (ms)
50% 5
66% 6
75% 6
80% 7
90% 8
95% 14
98% 17
99% 20
100% 38 (longest request)
Анализ результатов
Импортирование модулей вначале файла уменьшило время одного запроса на ~23%(!) (в сравнение с импортированием непосредственно в обработчик), что весьма существенно.
Такая большая разница в результатах кроется в работе функции require
. Перед импортированием, require
обращается к алгоритму поиска абсолютного пути к запрашиваемому компоненту (алгоритм описан в документации node.js). Когда путь был найден, то require
проверяет был ли закеширован модуль, и если нет — не делает ничего сверхестественного, кроме вызова обычного fs.readFileSync
для .js
и .json
форматов, и недокументированного process.dlopen
для загрузки C++ модулей.
Note: пробовал «прогревать» кеш для случая с непосредственным импортированием модулей в обработчик (перед запуском утилиты ab, модули были уже закешированы) — производительность улучшалась на 1–2%.
Выводы
Если вы используете node.js, как сервер (нет разницы какой — TCP/UDP или HTTP (S)), то:
- Импортирование всех модулей необходимо делать вначале файла, чтобы избегать лишних синхронных операций связанных с загрузкой модулей (один из главных анти-паттернов использования node.js как асинхронного сервера).
- Вы можете не тратить ресурсы на вычисление абсолютного пути запрашиваемого модуля (это и есть основное место для потери производительности).