A Path Down Node's Modules
Understanding how node modules are loaded in, and how they're cached.
Have you ever wondered why import
statements don't run your code twice?
This has to do with how node manages it's modules and how they are loaded into memory.
We'll see an example below on how that works in practice.
When we make statements like this in node.js:
// x.js
export const X = () => {};
// module-1.js
import { X } from './x';
We're actually telling node to "load in" the module at that file path. If we don't provide a relative path, it goes and fetches the module from node_modules if one exists.
We can add an console.log statement to see what is the "status" of our modules at any point. As an example, see below:
// module-1.js
import { X } from './x.js'
console.log(module);
export const someFunction = () => X;
Which logs the following information.
{
id: 'module-1',
filename: '/some/file/name/module-1.js',
loaded: false,
children: [
{
id: 'x',
path: '/some/file/name/x.js',
exports: { x: () => {} }
loaded: true,
}
]
}
This means, that our current module (id module-1
) has NOT loaded in fully yet, since loaded: false
.
BUT, our imports have loaded in before, loaded: true
for id: x
.
Synchronous Module Loading
Before the console.log in our code, we've loaded the imported modules fully, and we have not loaded the current module fully. If the imported modules have already been loaded before, node will take them from the memory instead.
Looking at the ECMAScript Abstract Module Records, we can see that in the spec, they define that if a module has already been loaded before, we do nothing, and instead just provide the module in-memory.
Do nothing if this module has already been evaluated. Otherwise, transitively evaluate all module dependences of this module and then evaluate this module. ModuleDeclarationInstantiation must be completed prior to invoking this method.
Asynchronous Module Loading
If X
was an asynchronous function that loads in something asynchronously, then we would need to make sure that we properly cache it somehow.
This could mean making a variable that we then set in an async
function like so:
let X;
export const getX = async () => {
if (X) {
return X;
}
X = await loadInX();
return X;
};
This still means that we create the function getX
synchronously, but what it does for us is asynchronous (like loading from a file, API, or buffers).
There are some utilities that you can use in node.js such as top-level awaits, but these can be quite dangerous to use in production. The main reason I say this is because of the statements that Node.js makes:
If a top level await expression never resolves, the node process will exit with a 13 status code.
If your top-level await is crucial for the application to run at all, then that seemse like a good idea to implement. However, if it's a side process or if your application can function without this top-level await, then it is better to have good instrumentation and logging, along with a cached local variable, that can report any issues with loading the async value.
~ Arya
Go back home.