skip to content

A CommonJS service takes several seconds to start serving traffic, and the time is spent in top-level code of required modules — parsing config, building lookup tables — before the server ever starts listening. Why does require() put that cost exactly there, and how would you restructure the modules?

level: seniorimportance: should knowfreq 36%

answer

  1. module bodies are code that runs
  2. charged to the requiring call
  3. serial, depth-first, one thread
  4. bodies declare, functions do
  5. explicit init beats implicit load

basics

~20 s

require() runs a module's body to completion synchronously before returning, so anything at a module's top level executes during the require call and blocks the single thread. Move expensive work into exported init or factory functions the application calls deliberately.

solid answer

~50 s

A module body is not a declaration list — it is code that runs, and `require` runs it synchronously to completion before handing back `module.exports`. So top-level work is charged to whoever requires the module, at the moment they require it, on the one thread, with nothing able to interleave. A tree of requires at the top of your entry file therefore runs the whole dependency graph's top-level code depth-first before your first real statement executes. The fix is a discipline, not a flag: keep module bodies to declarations and cheap constants, and move parsing, table building, connection setup and anything that can fail into an exported `init()` or factory the application calls when it is ready. That also lets you order and parallelise the expensive parts, report failures with context, and require the heavy module lazily inside the function that needs it if the work is only sometimes required.

code

javascript · 19 lines
javascript
// registry.js — require() returns immediately; the cost is the caller's to schedule
let table = null;

function buildTable(rows) {
  const t = new Map();
  for (const row of rows) t.set(row.id, row);
  return t;
}

module.exports = {
  init(rows) {
    table = buildTable(rows);
    return table;
  },
  lookup(id) {
    if (table === null) throw new Error('registry not initialised');
    return table.get(id);
  },
};

go deeper

for a junior

Know that a module's top-level code runs when it is required, not when its exports are first used, and that this is why heavy work at the top of a file slows down startup.

for a middle

Explain the mechanics of serial depth-first evaluation and why a synchronous require cannot await anything, then show the init-function shape that moves the work out of the module body.

for a senior

Demonstrate the full loop: instrument to attribute startup time per module, restructure to explicit initialisation, and weigh lazy requiring against fail-fast behaviour. Talk about failure reporting and test isolation, not just milliseconds.

for a principal

Frame load-time side effects as an architectural constraint: implicit ordering across a dependency tree is a coupling nobody declared. Decide the codebase-wide policy on what a module body may do and how startup is sequenced, so it holds as the tree grows.

## Why the cost lands where it lands CommonJS loading is synchronous and run-to-completion. `require('./x')` evaluates x.js's body from top to bottom, and only when the last statement has finished does it return the value in `module.exports`. There is no interleaving, no yielding, no partial load. If x.js itself requires y.js at its top, that nested call runs y.js to completion first, so loading walks the graph depth-first. The consequence for a service is direct: the top level of every module reachable from your entry file is a single, serial, blocking startup phase that runs before your own first statement. A module that does ```js // tariffs.js const raw = fs.readFileSync('./tariffs.json', 'utf8'); const table = buildIndex(JSON.parse(raw)); module.exports = { table }; ``` charges that read and parse to whoever requires it, whether or not they will ever consult a tariff. Multiply by a dependency tree and you get the symptom in the question. ## The diagnosis Before restructuring, confirm the shape. Log a timestamp as the first statement of the entry file and again immediately before the server starts, and instrument suspicious module bodies with a duration log at their top and bottom. Because loading is serial, a simple per-module elapsed time attributes the cost precisely — there is no concurrency to confuse the accounting. Sorting those numbers usually shows one or two offenders rather than diffuse slowness. ## Restructuring: bodies declare, functions do The design rule that follows is that a module body should *declare* things and an exported function should *do* things. ```js // registry.js — nothing expensive happens while require() runs let table = null; function buildTable(rows) { const t = new Map(); for (const row of rows) t.set(row.id, row); return t; } module.exports = { init(rows) { table = buildTable(rows); }, lookup(id) { if (table === null) throw new Error('registry not initialised'); return table.get(id); }, }; ``` This buys several things beyond a faster require. The application now controls *when* initialisation happens, so it can start cheap listeners first, or run several independent initialisations concurrently instead of serially. It controls *what happens on failure*: a throw inside `init()` carries application context and can be retried or degraded, whereas a throw at module top level unwinds out of a `require` call in the middle of the dependency graph with far less context. And initialisation can be asynchronous, which a module body genuinely cannot express in CommonJS — this is exactly why so many modules that need I/O expose an async `connect()` or `start()` rather than doing it at load. ## Lazy requiring The second lever is that `require` is an ordinary function call, so it can live inside the function that needs the module: ```js function renderReport(data) { const heavy = require('./chart-renderer'); // loaded on first report, not at startup return heavy.render(data); } ``` This moves a module's entire load cost off the startup path for features that are rarely or never exercised. It is a real technique, and it is also a trade-off worth naming out loud in an interview: the cost and any load-time failure now appear during a request rather than at startup, which is worse for fail-fast behaviour and can surprise operators. Use it for genuinely optional subsystems — a CLI subcommand, an export format, a debug tool — not as a blanket policy. ## Side effects at load are the deeper problem Slow startup is the visible symptom; the structural problem is that load-time side effects create implicit ordering. A module that opens a connection, registers a global handler, or mutates shared state at its top level makes its behaviour depend on *when it happens to be required*, which is a function of import order across the whole tree. That is hard to reason about, hard to test (importing the module in a test suite performs the side effect), and hard to change safely — reordering two requires can alter behaviour. Pushing side effects into explicit calls makes the ordering visible in the application's own code. ## What to say in an interview Lead with the mechanism — synchronous run-to-completion evaluation charges module top-level work to the requiring call — then give the restructuring rule, then name the trade-offs: explicit init costs a little ceremony and requires discipline about "not initialised" states; lazy requiring trades startup time for first-use latency and later failure detection. Mentioning testability and failure-reporting benefits shows you are treating it as a design property rather than a performance trick.

  • Why can't a CommonJS module body simply await its asynchronous initialisation?
    Because `require` is synchronous: it must return a value when the body ends, and a module body has no way to suspend and resume that call. Anything asynchronous started at the top level is therefore unfinished when importers receive the exports. The honest shapes are an exported async `init()`/`connect()` the application awaits, or exporting a promise that consumers await before use.
  • What are the downsides of moving a require inside the function that needs it?
    The load cost and any load-time failure move from startup to first use, so a latent problem surfaces during a request instead of during a deploy's health check, and one unlucky request pays the latency. It also hides a dependency from a reader scanning the top of the file. It is worth it for genuinely optional subsystems, not as a default.
  • Beyond startup time, what does load-time side-effecting cost you?
    Predictability and testability. A module that connects, registers a handler or mutates shared state at its top level behaves according to when it happened to be required, which depends on require order across the whole tree — so reordering two lines can change behaviour, and merely importing the module in a test performs the side effect. Explicit initialisation puts that ordering in the application's own code.

saying these in an interview costs you the question

  • Suggests requiring modules in parallel to speed up loading
  • Thinks top-level work runs lazily on first property access
  • Believes an async operation started at module top level completes before require returns
  • Treats lazy require as free rather than as moved cost
  • Says the fix is to make module bodies smaller without moving the work

context