Что быстрее: fs.readFileSync() или require()?

5 min read

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

  1. esm - стандартный модуль javascript, подключение через import
  2. cjs - это CommonJS модуль, подключение через require
  3. builtin - для загрузки CommonJS модулей из стандартной библиотеки NodeJS (например, fs, path, http)
  4. json - загрузка файлов формата JSON
  5. addon - загрузка C++ модулей
  6. 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. Если интересно - почитайте исходники