[Из песочницы] Правильное использование 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;

    // сделать что-то
}

Преимущество такого способа в том, что модули необходимые в обработчике явно импортированы внутри и не засоряют пространство имен модуля (особенно актуально, если импортируемый модуль нужен только в одном обработчике). Так же появляется возможность отложенного экспортирования данных модуля.

Основные минусы:


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


  1. Импортирование всех модулей необходимо делать вначале файла, чтобы избегать лишних синхронных операций связанных с загрузкой модулей (один из главных анти-паттернов использования node.js как асинхронного сервера).
  2. Вы можете не тратить ресурсы на вычисление абсолютного пути запрашиваемого модуля (это и есть основное место для потери производительности).

© Habrahabr.ru