Backend Development 15 min read

Deep Dive into Node.js CJS Module Loading Process

The article dissects Node.js v17’s source to reveal how the CommonJS `require` system is bootstrapped, how native modules are loaded, how `Module._load` resolves, caches, and executes user files, and how the overall CJS loading pipeline operates step‑by‑step.

DaTaobao Tech
DaTaobao Tech
DaTaobao Tech
Deep Dive into Node.js CJS Module Loading Process

This article explores the Node.js source code to gain a deep understanding of the CommonJS (CJS) module loading mechanism.

In Node.js, the require function is the API used to load CJS modules, even though the V8 engine itself does not provide a CJS system. The article starts by showing a typical require('fs') usage and then investigates how Node locates and loads modules.

The examined Node.js version is v17.x (git commit 881174e016d6c27b20c70111e6eae2296b6c6293 ). The entry point of the runtime is the node_main.cc file, where node::Start is called to start a Node instance:

int Start(int argc, char** argv) {
  InitializationResult result = InitializeOncePerProcess(argc, argv);
  if (result.early_return) {
    return result.exit_code;
  }
  {
    Isolate::CreateParams params;
    // ... omitted for brevity ...
    NodeMainInstance main_instance(&params, uv_default_loop(), ...);
    result.exit_code = main_instance.Run(env_info);
  }
  TearDownOncePerProcess();
  return result.exit_code;
}

The NodeMainInstance::Run method creates the main environment and then runs it:

int NodeMainInstance::Run(const EnvSerializeInfo* env_info) {
  Locker locker(isolate_);
  Isolate::Scope isolate_scope(isolate_);
  HandleScope handle_scope(isolate_);
  int exit_code = 0;
  DeleteFnPtr
env = CreateMainEnvironment(&exit_code, env_info);
  CHECK_NOT_NULL(env);
  Context::Scope context_scope(env->context());
  Run(&exit_code, env.get());
  return exit_code;
}

During environment creation, an Environment object is instantiated, which holds a NativeModule system for built‑in modules. The bootstrapping phase runs RunBootstrapping to load internal loaders:

MaybeLocal
Environment::RunBootstrapping() {
  CHECK(!has_run_bootstrapping_code());
  if (BootstrapInternalLoaders().IsEmpty()) {
    return MaybeLocal
();
  }
  Local
result;
  if (!BootstrapNode().ToLocal(&result)) {
    return MaybeLocal
();
  }
  // ... checks and finalization ...
  return scope.Escape(result);
}

The internal loader executes internal/bootstrap/loaders.js , which defines nativeModuleRequire , internalBinding and the small built‑in module system:

function nativeModuleRequire(id) {
  if (id === loaderId) return loaderExports;
  const mod = NativeModule.map.get(id);
  if (!mod) throw new TypeError(`Missing internal module '${id}'`);
  return mod.compileForInternalLoader();
}
const loaderExports = { internalBinding, NativeModule, require: nativeModuleRequire };
return loaderExports;

For user modules, the core of the loading logic resides in Module._load . The function resolves the filename, checks the cache, handles built‑in modules, and finally creates a new Module instance to execute the file:

Module._load = function(request, parent, isMain) {
  const filename = Module._resolveFilename(request, parent, isMain);
  if (StringPrototypeStartsWith(filename, 'node:')) {
    const id = StringPrototypeSlice(filename, 5);
    const module = loadNativeModule(id, request);
    if (!module?.canBeRequiredByUsers) throw new ERR_UNKNOWN_BUILTIN_MODULE(filename);
    return module.exports;
  }
  const cachedModule = Module._cache[filename];
  if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.loaded ? cachedModule.exports : getExportsForCircularRequire(cachedModule);
  }
  const module = new Module(filename, parent);
  Module._cache[filename] = module;
  module.load(filename);
  return module.exports;
};

The Module.prototype.load method selects the appropriate file extension handler ( .js , .json , .node ) and executes the module code. For .js files, the source is wrapped in a function and evaluated with the module’s own require , exports , and other locals.

Summarising, the CJS loading flow consists of:

Initialising Node and loading NativeModule for built‑in modules.

Running the internal bootstrap to expose require , internalBinding , etc.

Invoking the user entry point via run_main , which creates a Module and calls Module._load .

Recursively loading dependent modules through module.require and caching their exports .

Understanding this pipeline enables deeper exploration of other runtime aspects such as global variable initialisation and ES‑module handling.

backendNode.jssource code analysisCJSmodule-loadingRequire
DaTaobao Tech
Written by

DaTaobao Tech

Official account of DaTaobao Technology

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.