├── .gitignore ├── .npmrc ├── .travis.yml ├── README.md ├── index.js ├── package.json └── test ├── .gitignore ├── .npmrc ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | cache: 4 | directories: 5 | - node_modules 6 | notifications: 7 | email: false 8 | node_js: 9 | - '4' 10 | before_install: 11 | - npm i -g npm@^2.0.0 12 | before_script: 13 | - npm prune 14 | after_success: 15 | - npm run semantic-release 16 | branches: 17 | except: 18 | - "/^v\\d+\\.\\d+\\.\\d+$/" 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cache-require-paths 2 | 3 | > Caches resolved paths in module require to avoid Node hunting for right module. Speeds up app load. 4 | 5 | [![NPM][cache-require-paths-icon] ][cache-require-paths-url] 6 | 7 | [![Build status][cache-require-paths-ci-image] ][cache-require-paths-ci-url] 8 | [![semantic-release][semantic-image] ][semantic-url] 9 | 10 | [cache-require-paths-icon]: https://nodei.co/npm/cache-require-paths.png?downloads=true 11 | [cache-require-paths-url]: https://npmjs.org/package/cache-require-paths 12 | [cache-require-paths-ci-image]: https://travis-ci.org/bahmutov/cache-require-paths.png?branch=master 13 | [cache-require-paths-ci-url]: https://travis-ci.org/bahmutov/cache-require-paths 14 | [semantic-image]: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg 15 | [semantic-url]: https://github.com/semantic-release/semantic-release 16 | 17 | This is a partial solution to Node "hunting" for right file to load when you require a 3rd party 18 | dependency. See [Node’s `require` is dog slow](https://kev.inburke.com/kevin/node-require-is-dog-slow/) 19 | and [Faster Node app require](http://glebbahmutov.com/blog/faster-node-app-require/) for details. 20 | 21 | ## Use 22 | 23 | npm install --save cache-require-paths 24 | 25 | Load the module first in your application file 26 | 27 | ```js 28 | // index.js 29 | require('cache-require-paths'); 30 | ... 31 | ``` 32 | 33 | The first time the app loads, a cache of resolved file paths will be saved to `.cache-require-paths.json` 34 | in the current directory. Every application startup after that will reuse this filename cache to avoid 35 | "hunting" for the right filename. 36 | 37 | To save cached paths to a different file, set the environmental variable `CACHE_REQUIRE_PATHS_FILE`. 38 | 39 | ## Results 40 | 41 | Here are results for loading common packages without and with caching resolved require paths. 42 | You can run any of this experiments inside the `test` folder. `node index.js` loads 43 | using the standard resolve. `node index.js --cache` uses a cache of the resolves paths. 44 | 45 | Using node 0.10.37 46 | 47 | require('X') | standard (ms) | with cache (ms) | speedup (%) 48 | ------------------------------------------------------------------ 49 | express@4.12.3 | 72 | 46 | 36 50 | karma@0.12.31 | 230 | 170 | 26 51 | grunt@0.4.5 | 120 | 95 | 20 52 | sails@0.11.0 | 170 | 120 | 29 53 | 54 | Using node 0.12.2 - all startup times became slower. 55 | 56 | require('X') | standard (ms) | with cache (ms) | speedup (%) 57 | ------------------------------------------------------------------ 58 | express@4.12.3 | 90 | 55 | 38 59 | karma@0.12.31 | 250 | 200 | 20 60 | grunt@0.4.5 | 150 | 120 | 20 61 | sails@0.11.0 | 200 | 145 | 27 62 | 63 | ## TODO 64 | 65 | - [ ] Cache only the absolute paths (relative paths resolve quickly) 66 | - [ ] Invalidate cache if dependencies in the package.json change 67 | 68 | ## Discussion 69 | 70 | You can see Node on Mac OS X searchig for a file to load when loading an absolute path 71 | like `require(express)` by using the following command to make a log of all system level 72 | calls from Node (start this from another terminal before running node program) 73 | 74 | sudo dtruss -d -n 'node' > /tmp/require.log 2>&1 75 | 76 | Then run the test program, for example in the `test` folder run 77 | 78 | $ node index.js 79 | 80 | Kill the `dtruss` process and open the generated `/tmp/require.log`. It shows every system call 81 | with the following 4 columns: process id (should be single node process), relative time (microseconds), 82 | system call with arguments, and after the equality sign the numerical result of the call. 83 | 84 | When loading `express` dependency from the test program using `require('express');` we see 85 | the following search (I abbreviated paths for clarity): 86 | 87 | # microseconds call 88 | 664730 stat64(".../test/node_modules/express\0", 0x7FFF5FBFECF8, 0x204) = 0 0 89 | 664784 stat64(".../test/node_modules/express.js\0", 0x7FFF5FBFED28, 0x204) = -1 Err#2 90 | 664834 stat64(".../test/node_modules/express.json\0", 0x7FFF5FBFED28, 0x204) = -1 Err#2 91 | 664859 stat64(".../test/node_modules/express.node\0", 0x7FFF5FBFED28, 0x204) = -1 Err#2 92 | 664969 open(".../test/node_modules/express/package.json\0", 0x0, 0x1B6) = 11 0 93 | 664976 fstat64(0xB, 0x7FFF5FBFEC38, 0x1B6) = 0 0 94 | 665022 read(0xB, "{\n \"name\": \"express\", ...}", 0x103D) = 4157 0 95 | 665030 close(0xB) = 0 0 96 | 97 | By default, Node checks if the local `node_modules/express` folder exists first (first `stat64` call), 98 | Then it tries to check the status of the `node_modules/express.js` file and fails. 99 | Then `node_modules/express.json` file. Then `node_modules/express.node` file. Finally it opens 100 | the `node_modules/express/package.json` file and reads the contents. 101 | 102 | Note that this is not the end of the story. Node loader only loads `express/package.json` to fetch 103 | `main` filename or use the default `index.js`! Each wasted file system call takes only 100 microseconds, 104 | but the tiny delays add up to hundreds of milliseconds and finally seconds for larger frameworks. 105 | 106 | Profile the same program with `--cache` option added to the command line arguments 107 | 108 | $ node index.js --cache 109 | 110 | This option loads the `cache-require-paths` module as the first require of the application 111 | 112 | ```js 113 | var useCache = process.argv.some(function (str) { 114 | return str === '--cache'; 115 | }); 116 | if (useCache) { 117 | console.log('using filename cache'); 118 | require('cache-require-paths'); 119 | } 120 | ``` 121 | 122 | The trace now shows *no calls to find `express` package*, just straight load of the `express/index.js` file. 123 | 124 | 643466 stat64(".../node_modules/express/index.js\0", 0x7FFF5FBFED28, 0x3) = 0 0 125 | 643501 lstat64(".../node_modules\0", 0x7FFF5FBFED08, 0x3) = 0 0 126 | 643513 lstat64(".../node_modules/express\0", 0x7FFF5FBFED08, 0x3) = 0 0 127 | 643523 lstat64(".../node_modules/express/index.js\0", 0x7FFF5FBFED08, 0x3) = 0 0 128 | 643598 open(".../node_modules/express/index.js\0", 0x0, 0x1B6) = 12 0 129 | 643600 fstat64(0xC, 0x7FFF5FBFED58, 0x1B6) = 0 0 130 | 131 | Mission achieved. Note that the speedup only happens after the first application run finishes successfully. 132 | The resolution cache needs to be saved to a local file, and this happens only on process exit. 133 | 134 | ## Small print 135 | 136 | Author: Gleb Bahmutov © 2015 137 | 138 | * [@bahmutov](https://twitter.com/bahmutov) 139 | * [glebbahmutov.com](http://glebbahmutov.com) 140 | * [blog](http://glebbahmutov.com/blog) 141 | 142 | License: MIT - do anything with the code, but don't blame me if it does not work. 143 | 144 | Spread the word: tweet, star on github, etc. 145 | 146 | Support: if you find any problems with this module, email / tweet / 147 | [open issue](https://github.com/bahmutov/cache-require-paths/issues) on Github 148 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Module = require('module'); 2 | var fs = require('fs'); 3 | var exists = fs.existsSync; 4 | 5 | var _require = Module.prototype.require; 6 | var SAVE_FILENAME = 7 | process.env.CACHE_REQUIRE_PATHS_FILE ? 8 | process.env.CACHE_REQUIRE_PATHS_FILE : 9 | './.cache-require-paths.json'; 10 | var nameCache; 11 | try { 12 | nameCache = exists(SAVE_FILENAME) ? JSON.parse(fs.readFileSync(SAVE_FILENAME, 'utf-8')) : {}; 13 | } catch (err) { 14 | nameCache = {}; 15 | } 16 | 17 | var currentModuleCache; 18 | var pathToLoad; 19 | 20 | Module.prototype.require = function cachePathsRequire(name) { 21 | 22 | currentModuleCache = nameCache[this.filename]; 23 | if (!currentModuleCache) { 24 | currentModuleCache = {}; 25 | nameCache[this.filename] = currentModuleCache; 26 | } 27 | if (currentModuleCache[name] && 28 | // Some people hack Object.prototype to insert their own properties on 29 | // every dictionary (for example, the 'should' testing framework). Check 30 | // that the key represents a path. 31 | typeof currentModuleCache[name] === 'string' && 32 | fs.existsSync(currentModuleCache[name]) // the file must exist for a cache hit 33 | ) { 34 | pathToLoad = currentModuleCache[name]; 35 | } else { 36 | pathToLoad = Module._resolveFilename(name, this); 37 | currentModuleCache[name] = pathToLoad; 38 | } 39 | 40 | return _require.call(this, pathToLoad); 41 | }; 42 | 43 | function printCache() { 44 | Object.keys(nameCache).forEach(function (fromFilename) { 45 | console.log(fromFilename); 46 | var moduleCache = nameCache[fromFilename]; 47 | Object.keys(moduleCache).forEach(function (name) { 48 | console.log(' ', name, '->', moduleCache[name]); 49 | }); 50 | }); 51 | } 52 | 53 | process.once('exit', function () { 54 | try { 55 | fs.writeFileSync(SAVE_FILENAME, 56 | JSON.stringify(nameCache, null, 2), 'utf-8'); 57 | } catch (err) { 58 | console.error('cache-require-paths: Failed saving cache: ' + err.toString()); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache-require-paths", 3 | "description": "Caches resolved paths in module require to avoid Node hunting for right module. Speeds up app load.", 4 | "main": "index.js", 5 | "version": "0.0.0-semantic-release", 6 | "scripts": { 7 | "test": "cd test; npm install; node index; node index --cache; node index --cache", 8 | "semantic-release": "semantic-release pre && npm publish && semantic-release post", 9 | "size": "t=\"$(npm pack .)\"; wc -c \"${t}\"; tar tvf \"${t}\"; rm \"${t}\";" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/bahmutov/cache-require-paths.git" 14 | }, 15 | "files": [ 16 | "index.js" 17 | ], 18 | "keywords": [ 19 | "npm", 20 | "node", 21 | "application", 22 | "load", 23 | "startup", 24 | "performance", 25 | "speed", 26 | "cache", 27 | "resolution" 28 | ], 29 | "author": "Gleb Bahmutov ", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/bahmutov/cache-require-paths/issues" 33 | }, 34 | "homepage": "https://github.com/bahmutov/cache-require-paths", 35 | "devDependencies": { 36 | "pre-git": "0.2.1", 37 | "semantic-release": "^4.3.5" 38 | }, 39 | "pre-commit": [ 40 | "npm test", 41 | "npm version" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | .cache-require-paths.json 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /test/.npmrc: -------------------------------------------------------------------------------- 1 | registry=http://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var useCache = process.argv.some(function (str) { 2 | return str === '--cache'; 3 | }); 4 | 5 | if (useCache) { 6 | console.log('using filename cache'); 7 | require('..'); 8 | } 9 | 10 | function time(fn) { 11 | var t = process.hrtime(); 12 | var result = fn(); 13 | t = process.hrtime(t); 14 | var nanoToMs = 1e-6; 15 | console.log('benchmark took %d seconds and %d milliseconds', 16 | t[0], Math.round(t[1] * nanoToMs)); 17 | console.log('bencharm function was'); 18 | console.log(fn.toString()); 19 | } 20 | 21 | function load() { 22 | var exp = require('express'); 23 | } 24 | 25 | time(load); 26 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cache-require-source-test", 3 | "version": "0.1.0", 4 | "description": "test a couple of requires load", 5 | "main": "index.js", 6 | "devDependencies": { 7 | "express": "4.12.3", 8 | "grunt": "0.4.5", 9 | "karma": "0.12.31", 10 | "sails": "0.11.0" 11 | } 12 | } 13 | --------------------------------------------------------------------------------