Предисловие
Не так давно проект, на котором я работаю в данный момент, начал использовать модульную систему 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 как асинхронного сервера).
- Вы можете не тратить ресурсы на вычисление абсолютного пути запрашиваемого модуля (это и есть основное место для потери производительности).
