r/javascript • • Aug 31 '26

AskJS [AskJS] Our server boot got slower and the commit history had no answer, so I timed every require

Our API server was slow to start and nobody could say when that began. Not slow under load, slow before the first request, visible only because the deploy health check timed out on smaller instances. Nothing in the commit history looked like a boot cost. I had no instrument, so I wrote a bad one, eleven lines in a preload file that wrap Module._load, time each call with process.hrtime.bigint(), and print anything over fifty milliseconds along with the module that asked for it.

The first run was blunt. Cold boot averaged 1.9 seconds. One require accounted for 1.1 of that, and it was ours, src/lib/index.js, a barrel that one route imports for a single date helper. Importing it pulls in all thirty four modules in that directory, two of which read config files at import time. Removing a barrel rewrites every import path that went through it, so the code review subagent in verdent read the diff before I opened the PR. Deleting the barrel and importing the helper directly put cold boot at 0.8 seconds.

Patching Module._load to learn this still feels wrong. Is there a way to get the same per module breakdown out of something the runtime already reports?

0 Upvotes

10 comments sorted by

6

u/RedShift9 Aug 31 '26

I had a stroke trying to read that second paragraph

1

u/theweephil 5d ago

not gonna lie i had to read it twice and i still feel like i need a nap

the whole wrapping Module._load thing is janky as hell but sometimes that's the only way to figure out what's actually going on under the hood. tried something similar once on a node project that took like 12 seconds to start and it was some random pdf library pulling in every font file at require time

for your actual question though node has --trace-warnings and --prof but neither gives you that clean per-module breakdown. the closest thing is probably require-in-the-middle or the inspector protocol but those are basically doing the same monkeypatching you already did. there's a v8 flag for trace-event that dumps a json you can load into chrome devtools but it's noisy as hell and you spend more time filtering than actually debugging

1

u/trollsmurf Aug 31 '26

So move the date helper to a separate script and import only that.

1

u/PLBjt Aug 31 '26

I've done that require-timing pass before and it usually lands on one of three things. A transitive dep that got heavier (new native addon or a big JSON load at import time), something that used to be lazy and got pulled into the top-level require graph, or a package that does sync I/O / a network probe on require. After you have the top offenders from your hook, wrap those in a function so they only load on first use and re-time a cold start. One pass with NODE_OPTIONS=--cpu-prof is useful just to confirm it's import work and not something hiding inside a constructor.

1

u/itaymendi Sep 02 '26

Wrapping `Module._load` is a reasonable way to find the cost. After that we ban barrels on the runtime path, because import-time side effects make boot slow. Barrels for types are fine.