Давайте разберемся, как правильно хранить и оптимальным образом считывать файлы с текстовыми данными в Node.js: это могут быть какие-то конфиги, SQL или GraphQL-запросы и другие статичные строки.
На ум приходят два решения: подключать файл как JS-модуль через функцию require или считывать с файловой системы через модуль fs. Мы решили сделать сравнительный тест и определить, какой способ оптимальнее и быстрее.
Для примера возьмем файл с простым GraphQL-запросом.
Вариант 1: requre('')
Обернем файл в кавычки и экспортируем, создав обычный JavaScript-модуль test.graphql.js
module.exports = `query GetSmth{
smths{
name,
params,
flag1,
flag2
}
}`
Подключать такой файл будем через функцию require()
const str = require('./test.graphql.js')
Вариант 2: fs.readFileSync('')
Будем работать с обычным текстовым файлом test.graphql:
query GetSmth{
smths{
name,
params,
flag1,
flag2
}
}
Считывать его будем через модуль fs:
const str = fs.readFileSync('./test.graphql', 'utf8');
Проверим скорость работы обоих вариантов двумя способами: будем замерять время выполнения nodejs-скрипта целиком, и отдельно посмотрим на время выполнения каждой из двух функций. А также проверим скорость подключения модулей json и es6 (через import)
В NodeJs существует несколько способов подключения модулей в код (и стратегии для каждого отличаются), давайте кратко их рассмотрим.
- esm - стандартный модуль javascript, подключение через import
- cjs - это CommonJS модуль, подключение через require
- builtin - для загрузки CommonJS модулей из стандартной библиотеки NodeJS (например, fs, path, http)
- json - загрузка файлов формата JSON
- addon - загрузка C++ модулей
- dynamic - так называемые, динамические модули
Стоит отметить, что загрузка ES6 модулей стала доступна в NodeJS с версии 8.5.0, но доступна только с флагом --experimental-modules
Проверка времени выполнения скрипта
Воспользуемся простым bash-скриптом для оценки времени выполнения nodejs-скриптов. Сами скрипты представлены чуть ниже.
#! /bin/bash
timestamp() {
date +"%s%N"
}
t1=$(timestamp)
node ./$1
t2=$(timestamp)
echo $(($t2-$t1))
Результат: при небольших размерах подключаемого файла - время для require и fs примерно одинаковое. При больших размерах - fs работает быстрее. Характер зависимости времени выполнения от размера файла для require и import совпадает (как и линии на графике), однако require работает немного быстрее. Json работает значительно лучше чем require, но хуже чем fs. Кстати, при подключении json модуля явно вызывается fs.readFileSync, в то время как для require\import используется напрямую вызов функций из библиотеки libuv (не забываем, что каждая из стратегий, описанных выше - работает по разному).
Проверка времени выполнения операции
Второй вариант проверки. Будем сравнивать время выполнения операции fs и require.
// require-test.js
let prev = (new Date()).getTime();
let x = require('./stub.js');
let curr = (new Date()).getTime();
console.log(curr - prev);
// fs-test.js
let fs = require('fs');
let prev = (new Date()).getTime();
let x = fs.readFileSync('stub');
let curr = (new Date()).getTime();
console.log(curr - prev);
В этом тесте fs всегда быстрее
Описание теста
Полные результаты проведения тестов приведены в таблице (графики выше строятся по этим данным). Код теста можно найти в репозитории, можете повторить его на своей машине.
Сначала определяются node-скрипты, которые будут тестироваться. Генерируются необходимые размеры файлов (в нашем случае количество байт по степени двойки) в файле sizes. При необходимости данный файл можно дополнить вручную любыми другими значениями размера файлов. Чтобы провести тест, необходимо поочередно запустить "runner"-ы для первого и второго варианта проверки. Данные скрипты запускают циклы: для каждого размера из sizes генерируется строка соответствующего размера, а затем используется в обычном текстовом файле, js-модуле и json-файле. Для повышения точности каждый замер времени проводится по 5 раз, а результатом берется среднее из пяти.
Заключение
При малых размерах файла сказывается время, необходимое для того, чтобы зареквайрить сам fs в исполняемый код, поэтому разница практически отсутствует, но при больших размерах файлов fs.readFileSync() работает быстрее, чем require().
P. S. Исходный код функции fs.readFileSync и require. Вот тут реализация import. Если интересно - почитайте исходники