Skip to content

fix(perf): do not list network interfaces when loading config definitions - #10097

Open
azchohfi wants to merge 1 commit into
npm:latestfrom
azchohfi:perf/lazy-local-address
Open

azchohfi wants to merge 1 commit into
npm:latestfrom
azchohfi:perf/lazy-local-address

Conversation

@azchohfi

@azchohfi azchohfi commented Oct 8, 2026

Copy link
Copy Markdown

Related: #10084 (Windows shims start node once per call) and #10096
(arborist and pacote on first use). Each removes a different part of npm's start-up cost; they
touch different files and the savings add up.

Summary

@npmcli/config/lib/definitions/definitions.js builds the allowed values of local-address at
module load:

'local-address': new Definition('local-address', {
  default: null,
  type: getLocalAddresses(),   // Object.values(os.networkInterfaces())...

So every npm command, npm --version included, enumerates the machine's network interfaces before
it does anything else. On Windows, os.networkInterfaces() goes through GetAdaptersAddresses and
takes about 7.5 ms on the test machine; it grows with VPN and virtual adapters. The list is needed
only when a local-address value is validated, which almost no command does. bin/npx-cli.js
also reads every definition's type to find boolean switches, so npx builds the list too.

This PR:

  1. definitions.js: local-address's type becomes a getter that builds the list on first
    read and memoizes it. hint and usage are given explicitly, so Definition does not read
    type to derive them.
  2. definition.js: the constructor copies property descriptors instead of using
    Object.assign, so a getter stays a getter. For data properties the two are the same.
  3. index.js, getTypesFromDefinitions: a definition whose type is an accessor becomes an
    accessor in types too. nopt reads types[key] only for keys present in argv, env or npmrc,
    so the list is built only when local-address is actually set.
  4. bin/npx-cli.js: skips accessor types when it collects boolean switches. A lazily
    computed type is a list of values, never a switch. It uses no optional chaining, because the
    bin scripts must parse on old Node.

os.networkInterfaces() calls per command go from 1 to 0 for npm --version, npm run <script>
and npx <bin>.

Numbers

  • Setup:
    • npm 12.2.0 from npm pack of latest (b317f16), against the same tarball with this commit;
    • official Node.js 26.7.0; Windows 11; i9-14900K;
    • a Dev Drive (ReFS), and the system drive (NTFS, Defender real-time scanning).
  • Method: interleaved passes after warm-up; medians in ms; the paired per-pass difference
    with its 95% CI.
Windows 11 passes before after saved
npm --version, Dev Drive 60 119.8 112.3 6.9 (4.7–9.8)
npm --version, system drive 60 149.7 137.9 9.3 (5.9–11.5)
npm run noop (node -e 0), Dev Drive 60 214.7 213.1 10.1 (5.4–15.0)
npm run noop, system drive 60 264.1 255.6 8.2 (0.7–15.3)

A second 60-pass run, in a clean user environment on a quieter machine, agrees: npm --version
saves 7.5 ms (6.7–8.6) on the Dev Drive and 10.4 ms (7.3–11.9) on the system drive; npm run noop,
5.0 ms (2.7–8.1) and 7.6 ms (6.2–9.8).

npx --no-install tsc --version (about 485 ms) is too noisy to resolve 7 ms in 40 passes: 3.0
(−7.3–19.0). A hook on os.networkInterfaces() shows the call gone there too.

The change was also tested on Linux: the call count goes from 1 to 0 and nothing regresses.

Behaviour

  • Identical output and exit codes, before and after, on Windows and Linux:
    • npm --version;
    • npm config get local-address with a valid --local-address, an invalid one (the same
      warning and null), and one from npm_config_local_address;
    • npm config ls -l.
  • Only change: definitions['local-address'].usage. It was built from the machine's own
    addresses (--local-address <…|…>) and is shown nowhere: no command lists local-address among
    its params, and the docs use typeDescription ("IP Address"). It is now
    --local-address <local-address>.

Tests

New tests, each failing without its change:

  • workspaces/config/test/definitions/definitions.js: the interfaces are not listed when the
    definitions load, nor by reading hint/usage. They are listed once on the first type read.
  • workspaces/config/test/index.js: getTypesFromDefinitions keeps an accessor type lazy. A
    Config does not read it when the key is not set, and validates against it when it is set.
  • test/bin/npx-cli.js: npx does not read a lazily typed definition.

Results on Windows 11, Node 26.7.0:

  • node . run test -w workspaces/config: passes, 100% coverage;
  • node . run lint: clean;
  • node . run test: passes except test/bin/windows-shims.js, test/lib/commands/publish.js
    and test/lib/commands/stage/index.js, which fail the same way on unmodified latest on the
    test machine (a PowerShell profile message, and scripts that call touch).

On Linux, the config workspace passes with 100% coverage.

Composition

…ions

The `local-address` definition built its list of allowed values with
`os.networkInterfaces()` when the definitions module loaded, so every
npm command enumerated the machine's network interfaces at start-up.
The list is only needed to validate a `local-address` value.

- `local-address`'s `type` is now a memoized getter, with `hint` and
  `usage` given so that `Definition` does not read it to derive them
- `Definition` copies property descriptors, so a getter stays a getter
- `getTypesFromDefinitions` keeps an accessor type as an accessor; nopt
  only reads the types of keys that are set
- `bin/npx-cli.js` skips accessor types when it collects boolean
  switches, so npx does not compute the list either

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant