Skip to content

modules: significantly improve require performance

This is a significant performance boost for require in all cases but mainly for loaded modules. This came up recently as a botteneck for @jasnell and @mcollina as far as I know.

Loading relative or absolute paths now has the same performance profile and loading node modules will be significantly faster than before.

The native startup won't be impacted by this but this should improve the startup time from an actual application due to this performance improvement. It is now also possible to use require lazily without sacrificing performance.

I introduced a new cache which sits on top of the former main cache and caches the filename of already loaded modules. This is then used to check for lazy require calls and for identical require calls from different files in the same directory.

I repaired the stat cache which was broken due to a minor mistake in a former commit. While doing that I just used a hard limit instead of the actual former implementation.

I removed multiple redundant path.resolve calls and refactored code for simplicity and less code branches where possible.

The extensions are now behind a proxy as otherwise it's necessary to recompute them each time a file can not be required without the extension (which is the default for most calls).

The Module._pathCache is now going to cache all requests in combination with the path including failed requests. At the same time I removed the package cache which is now obsolete due to that.

I tried to stay backwards compatible while some improvements required me change DEP0019 (require('.') resolved outside directory) to end-of-life (this was already the case but that was reverted due to a mistake in the implementation). I also slightly changed the _resolveLookupPaths and _resolveFilename implementations. Both got a new argument in 2017 while these have not been used so far in the wild as far as I can tell (I checked Gzemnid and could not find any hits about these arguments). This is clearly semver-major and even though I am able to backports some parts of this, there's always a chance that even different caching might have a slightly different behavior for users than the one before.

I also documented an existing behavior which was not yet documented: placing new entries in the require.cache.

I plan on splitting this up in smaller commits but I wanted to get some general feedback first.

One thing we might want to have a look at again is how much we cache and if we want to clear some caches after each require has fully resolved (including the child require calls) or if we are just fine keeping everything in the cache.

                                                                                   confidence improvement accuracy (*)    (**)   (***)
 module/load-native.js useCache='false' n=1000 path='util'                                ***     10.44 %       ±1.75%  ±2.31%  ±2.99%
 module/load-native.js useCache='false' n=1000 path='vm'                                  ***     10.49 %       ±1.27%  ±1.68%  ±2.17%
 module/load-native.js useCache='true' n=1000 path='util'                                 ***     10.99 %       ±1.12%  ±1.49%  ±1.92%
 module/load-native.js useCache='true' n=1000 path='vm'                                   ***     11.23 %       ±1.18%  ±1.56%  ±2.01%
 module/module-loader-deep.js cache='false' files=1000 ext='.js'                          ***      7.96 %       ±0.75%  ±0.99%  ±1.28%
 module/module-loader-deep.js cache='false' files=1000 ext=''                             ***      8.91 %       ±0.72%  ±0.96%  ±1.24%
 module/module-loader-deep.js cache='true' files=1000 ext='.js'                           ***     71.50 %       ±3.99%  ±5.28%  ±6.80%
 module/module-loader-deep.js cache='true' files=1000 ext=''                              ***     68.78 %       ±2.46%  ±3.25%  ±4.19%
 module/module-loader.js cache='false' n=1 files=500 dir='abs' name=''                    ***      6.34 %       ±0.94%  ±1.24%  ±1.60%
 module/module-loader.js cache='false' n=1 files=500 dir='abs' name='/'                   ***      6.81 %       ±0.94%  ±1.24%  ±1.60%
 module/module-loader.js cache='false' n=1 files=500 dir='abs' name='/index.js'           ***      3.22 %       ±1.00%  ±1.32%  ±1.70%
 module/module-loader.js cache='false' n=1 files=500 dir='rel' name=''                    ***     10.32 %       ±0.92%  ±1.21%  ±1.56%
 module/module-loader.js cache='false' n=1 files=500 dir='rel' name='/'                   ***     12.05 %       ±1.19%  ±1.57%  ±2.02%
 module/module-loader.js cache='false' n=1 files=500 dir='rel' name='/index.js'           ***      9.12 %       ±0.93%  ±1.23%  ±1.59%
 module/module-loader.js cache='false' n=1000 files=500 dir='abs' name=''                 ***     64.34 %       ±0.60%  ±0.80%  ±1.03%
 module/module-loader.js cache='false' n=1000 files=500 dir='abs' name='/'                ***     97.44 %       ±0.76%  ±1.01%  ±1.30%
 module/module-loader.js cache='false' n=1000 files=500 dir='abs' name='/index.js'        ***     71.51 %       ±0.70%  ±0.93%  ±1.20%
 module/module-loader.js cache='false' n=1000 files=500 dir='rel' name=''                 ***    413.20 %       ±2.71%  ±3.60%  ±4.68%
 module/module-loader.js cache='false' n=1000 files=500 dir='rel' name='/'                ***    441.79 %       ±1.71%  ±2.26%  ±2.93%
 module/module-loader.js cache='false' n=1000 files=500 dir='rel' name='/index.js'        ***    473.27 %       ±1.50%  ±1.99%  ±2.58%
 module/module-loader.js cache='true' n=1 files=500 dir='abs' name=''                     ***    205.70 %       ±3.58%  ±4.74%  ±6.12%
 module/module-loader.js cache='true' n=1 files=500 dir='abs' name='/'                    ***    112.56 %       ±2.63%  ±3.48%  ±4.48%
 module/module-loader.js cache='true' n=1 files=500 dir='abs' name='/index.js'            ***     98.16 %       ±4.61%  ±6.13%  ±7.96%
 module/module-loader.js cache='true' n=1 files=500 dir='rel' name=''                     ***    297.89 %       ±3.93%  ±5.21%  ±6.75%
 module/module-loader.js cache='true' n=1 files=500 dir='rel' name='/'                    ***    312.46 %       ±4.15%  ±5.50%  ±7.11%
 module/module-loader.js cache='true' n=1 files=500 dir='rel' name='/index.js'            ***    584.59 %      ±10.41% ±13.77% ±17.75%
 module/module-loader.js cache='true' n=1000 files=500 dir='abs' name=''                  ***     88.07 %       ±1.00%  ±1.33%  ±1.72%
 module/module-loader.js cache='true' n=1000 files=500 dir='abs' name='/'                 ***    125.64 %       ±0.65%  ±0.86%  ±1.11%
 module/module-loader.js cache='true' n=1000 files=500 dir='abs' name='/index.js'         ***     89.21 %       ±0.49%  ±0.65%  ±0.84%
 module/module-loader.js cache='true' n=1000 files=500 dir='rel' name=''                  ***    625.42 %       ±2.44%  ±3.23%  ±4.19%
 module/module-loader.js cache='true' n=1000 files=500 dir='rel' name='/'                 ***    612.17 %       ±1.88%  ±2.49%  ±3.23%
 module/module-loader.js cache='true' n=1000 files=500 dir='rel' name='/index.js'         ***    608.03 %       ±1.60%  ±2.13%  ±2.76%
Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

Merge request reports

Loading