Node.js: refreshing a module using require.cache
This is purely in reference to CommonJS modules.
TLDR
Before I bore you with why I did this, refreshing a module can be done by deleting its reference in the require.cache
object. Like so:
require('./some-module')
// use module
delete require.cache[require.resolve('./some-module')]
// use reloaded module
Simples. But there are caveats. But first.
Why was I interested in refreshing node modules?
If you have a small project, you can just use nodemon or node -—watch
to watch for file changes, but it restarts the whole application from scratch.
You lose state.
You have to wait for the application to restart
Now I wasn’t worried about state. That’s not a major problem for me. But application start up time was.
I could try to improve application start up time, or, I could reload only the files I work on. But before I could use require.cache
, I needed to understand a few core principles about it first.
Caching
Probably, old news, but Node caches modules you require. It’s great for performance as the module only needs to be loaded once. Let’s take the following CommonJS example.
We call console.log
in our child.js
file.
// child.js
console.log('hi', Date.now())
And let’s say we reference our child.js
file in our main.js
.
// main.js
require('./child')
require('./child')
If we run node main.js
, we get our hi
log with the timestamp:
hi 1731770123765
Notice, we’ve required child.js
twice, but it only gets ran once.
This is because the module is loaded into require.cache
. And the cached version is used on the second require.
And what does the cache look like?
[Object: null prototype] {
'/Users/username/Workspace/project/00_required/main.js': {
id: '.',
path: '/Users/username/Workspace/project/00_required',
exports: {},
filename: '/Users/username/Workspace/project/00_required/main.js',
loaded: false,
children: [ [Object] ],
paths: [
'/Users/username/Workspace/project/00_required/node_modules',
'/Users/username/Workspace/project/node_modules',
'/Users/username/Workspace/node_modules',
'/Users/username/node_modules',
'/Users/node_modules',
'/node_modules'
],
[Symbol(kIsMainSymbol)]: true,
[Symbol(kIsCachedByESMLoader)]: false,
[Symbol(kIsExecuting)]: true
},
'/Users/username/Workspace/project/00_required/child.js': {
id: '/Users/username/Workspace/project/00_required/child.js',
path: '/Users/username/Workspace/project/00_required',
exports: {},
filename: '/Users/username/Workspace/project/00_required/child.js',
loaded: true,
children: [],
paths: [
'/Users/username/Workspace/project/00_required/node_modules',
'/Users/username/Workspace/project/node_modules',
'/Users/username/Workspace/node_modules',
'/Users/username/node_modules',
'/Users/node_modules',
'/node_modules'
],
[Symbol(kIsMainSymbol)]: false,
[Symbol(kIsCachedByESMLoader)]: false,
[Symbol(kIsExecuting)]: false
}
}
But what if we want to invalidate the cache for our scenario - reloading on the files that have changed? That way we get the child.js
console to run twice.
Well, we can just delete it.
Invalidating the cache
If we delete the cache before requiring the file again, we can get Node to re-load the file as it can’t find it in the cache.
// main.js
require('./child')
delete require.cache[require.resolve('./child')]
require('./child')
Then we can see our file has been reloaded and we get two console commands, with two different timestamps.
hi 1731749319540
hi 1731749319548
But can this help with only loading the files I need for development. Well, it depends.
But what are the caveats?
Things get a little interesting when you export functions or variables from the child file and reference them in the parent file through a variable. What do I mean?
First, imagine child.js
now exports a function.
// child.js
module.exports = () => console.log('hi', Date.now())
Second, imagine the main.js
not only requires the function but assigns it to a constant. Well, you cannot use a constant again in the same blocked scope:
// main.js
const child = require('./child')
child()
delete require.cache[require.resolve('./child')]
child = require('./child')
child()
You get this error:
hi 1731772783273
/Users/username/Workspace/project/08_required/main.js:6
child = require('./child')
^
TypeError: Assignment to constant variable.
You say, you can solve this by using let
instead, but 1) not being able to use const
isn’t a good solution if you’re using it for immutability and 2) even if you use let
, you will need reassign the required variables. And I envisioned this getting rather messy.
But Jest deletes the cache without any problems!
Correct, but notice how they use it in their docs.
beforeEach(() => {
jest.resetModules();
});
test('works', () => {
const sum = require('../sum');
});
test('works too', () => {
const sum = require('../sum');
// sum is a different copy of the sum module from the previous test.
});
Notice how the required module is being required inside a function. It’s being lazy-loaded. So instead of being required and cached on application start up, it’s only required when the function runs.
For testing this is okay, performance is not as critical although waiting forever for tests aren’t fun either, but :
In production, initial requests will get I/O latency before the required modules are cached.
We’d need to use anonymous functions that could be refreshed to avoid figuring out how to reassign variables again.
It would require changing how we write code to require modules in a function which goes against the conventions of how Nodejs was designed to be written.
So is lazy-loading is out of the question?
Well, you would have to require modules in functions like this.
// abc.js
module.exports = () => {
console.log('abc2 module loaded at ', Date.now());
};
// child.js
module.exports = () => {
const abc = require('./abc')
};
And someone achieved this in his ExpressJs development environment by being a little more clever about what exactly he lazy-loaded.
// server/index.js
const express = require("express");
const port = parseInt(process.env.PORT, 10) || 3000;
// ...
// File watcher could go here
// ...
const app = express();
//Hot reload!
//ALL server routes are in this module!
app.use((req, res, next) => {
require("./app/router")(req, res, next);
});
//...
app.listen(port, err => {
if (err) throw err;
console.log(`> Ready on http://localhost:${port}`);
});
Notice:
The initial request would have a slower load time due to needing to load
require("./app/router")
. But subsequent requests would be cached!And that he separated the router handling into its own file in an anonymous function.
Disclaimer: I haven’t tried his solution yet, I’ll leave that for another article.
So can we achieve some sort of hot-module-reloading in native Node.js
It’s possible but it’s awkward to achieve fully based on what I’ve investigated so far. It can be done however partially. Hopefully I’ll share that article soon.