A node --run gyorsabb lehet az npm run parancsnál – de nem ugyanazt futtatja

A node --run közvetlenül indítja a package.json szkriptjeit, ezért gyorsabb lehet az npm run parancsnál. A váltás előtt azonban ellenőrizni kell az életciklus-hookokat és az npm környezeti változóit.

A package.json szkriptjeit nemcsak az npm run tudja elindítani. A Node.js 22 óta maga a Node is képes rá, csomagkezelő nélkül:

node --run test
node --run build -- --watch

A rövidebb út érezhetően gyorsabb lehet, különösen akkor, ha sok apró szkript indul egymás után, a két parancs azonban nem csereszabatos, mert a node --run szándékosan kevesebbet csinál, és a kihagyott lépésekre nem figyelmeztet.

Diagram: felül az npm run láncolt pre-teszt-post lépéssora egy lassú piros órával a végén, alul a node --run egyetlen közvetlen lépése gyors zöld órával és sebességvonalakkal.

Mit csinál a node --run?

A Node.js CLI dokumentációja szerint a kapcsoló megkeresi a package.json scripts mezőjében a megadott parancsot, majd lefuttatja, az argumentumokat pedig a -- után adja tovább a szkriptnek.

A funkció a Node.js 22.0.0-ban jelent meg 2024. április 24-én, és mivel a Node közvetlenül keresi meg, majd futtatja a szkriptet, nem indítja el az npm parancssori felületét, nem olvassa be annak teljes konfigurációját, valamint az npm által megszokott környezetet sem építi fel.

A különbség főleg rövid szkripteknél látszik

Az indítási többletidőt egy olyan szkripttel mértem meg, amely azonnal kilép: node -e "process.exit(0)". Node.js 22.22.3 és npm 10.9.8 alatt húsz egymást követő futtatásból ezt kaptam:

npm run hello × 20:   ≈ 3,02 s
node --run hello × 20: ≈ 0,76 s

Ezen a gépen tehát nagyjából négyszeres volt a különbség, ez azonban nem általános benchmark, csupán az indítási költség szemléltetése: egy többperces buildnél a megtakarított néhány tizedmásodperc eltűnik a zajban, monorepóban, sok workspace-en végigfutó CI-lépéseknél vagy gyakran újrainduló fejlesztői szkripteknél viszont már összeadódhat.

A pre- és post-szkriptek kimaradnak

Az npm életciklusához hozzátartozik, hogy egy test szkript körül automatikusan lefuttatja a pretest és posttest parancsokat is, ha léteznek, amit az npm szkriptekről szóló dokumentáció is rögzít.

{
  "scripts": {
    "pretest": "npm run lint",
    "test": "node --test",
    "posttest": "npm run report"
  }
}

Az npm run test ebben a példában mindhárom lépést sorban végrehajtja, míg a node --run test csak a középsőt indítja el, és sem hibát, sem figyelmeztetést nem ír ki, hiszen ez dokumentált, szándékos korlátozás.

A különbség könnyen elbújik egy CI-módosításban, mert ha a lintelés vagy a riportkészítés életciklus-hookban él, a parancs egyszerű cseréjével az ellenőrzés csendben kiesik a pipeline-ból.

Az npm környezeti változói sem jelennek meg

Az npm futás közben több npm_* kezdetű környezeti változót állít be, ilyen például az npm_package_version és az npm_lifecycle_event, amelyekből a build- és kiadási szkriptek gyakran a verziót vagy az aktuális életciklus-eseményt olvassák ki.

npm run showvars:
  npm_package_version=1.2.3
  npm_lifecycle_event=showvars

node --run showvars:
  npm_package_version=undefined
  npm_lifecycle_event=undefined

A hiányzó változó önmagában nem feltétlenül állítja le a folyamatot, ezért egy gondatlan, Docker-image-et címkéző szkript akár az undefined szót is beépítheti a címkébe; a migráció előtt tehát érdemes rákeresni a szkriptjeinkben a process.env.npm_ használatára, és nem pusztán arra hagyatkozni, hogy a parancs sikeresen lefut.

A Node saját változókat ad

A node --run két dokumentált, a Node.js 22.3.0 óta elérhető környezeti változót állít be: a NODE_RUN_SCRIPT_NAME a futtatott szkript nevét, a NODE_RUN_PACKAGE_JSON_PATH pedig a feldolgozott package.json elérési útját tartalmazza.

Ugyanettől a verziótól a parancs felfelé halad a könyvtárfában, amíg talál egy package.json-t. Az érintett szülőkönyvtárak node_modules/.bin mappáit hozzáadja a PATH-hoz, ezért egy mélyebben fekvő workspace-szkript a gyökérben telepített binárisokat is elérheti.

Mikor érdemes váltani?

Jó jelölt lehet egy önálló build-, teszt- vagy segédszkript, amelyet gyakran indítunk, és nem támaszkodik az npm környezetére, a csere előtt azonban három dolgot mindenképpen végignéznék:

  • tartozik-e a szkripthez pre vagy post hook;
  • olvas-e npm_* környezeti változót;
  • elérhető-e minden szükséges bináris a node --run által felépített PATH-on.

Ha mindhárom rendben van, a node --run egyszerű és gyors eszköz, egy örökölt, sok életciklus-hookból felépülő szkriptrendszernél viszont nem gyorsabb npm-helyettesítő, hanem eltérő viselkedésű futtató; a valódi kérdés ezért nem az, mennyivel hamarabb indul el, hanem az, hogy ugyanazt a munkát indítja-e el.

Források