Node.js 앱이 시작이 느립니다. 어떤 모듈을 탓해야 할지 모릅니다.
Source: Dev.to
30초 안에 보는 문제
Node.js는 시작이 왜 느린지 이유를 알려주지 않습니다. 총 부팅 시간이라는 하나의 숫자만 제공하고, 세부적인 분석은 전혀 없습니다.
그 사이에:
- 단일
require('sequelize')만으로도 400 ms가 조용히 추가될 수 있습니다. - 전이 의존성이 쌓이면서 — 하나를
require하면 Node가 300개의 모듈을 로드합니다. - 모듈 스코프에서 동기 작업(파일 읽기, 템플릿 컴파일, DB 연결 등)이 이벤트 루프를 차단해 애플리케이션이 시작되기 전까지 지연을 발생시킵니다.
- 캐시된 모듈이라도 의존성 그래프에 엣지를 추가해 실제 병목을 가립니다.
이 문제는 그 어느 때보다 중요합니다. Lambda에서 실행 중이라면(콜드 스타트가 이제 청구됩니다), 서버리스 플랫폼이나 제로에서 스케일링되는 컨테이너에서도 — 시작 시간은 첫 번째 요청에서 사용자가 체감하는 지연이 됩니다.
Source:
coldstart가 실제로 하는 일
다음 명령을 어떤 Node 앱에든 실행해 보세요:
npx @yetanotheraryan/coldstart server.js
그러면 다음과 같은 결과가 나타납니다:
coldstart — 847ms total startup
┌─ express 234ms ████████████░░░░░░░░
│ ├─ body-parser 89ms █████░░░░░░░░░░░░░░░
│ ├─ qs 12ms █░░░░░░░░░░░░░░░░░░░
│ └─ path-to-regex 8ms ░░░░░░░░░░░░░░░░░░░░
├─ sequelize 401ms █████████████████████ ⚠ slow
│ ├─ pg 203ms ███████████░░░░░░░░░
│ └─ lodash 98ms █████░░░░░░░░░░░░░░░
└─ dotenv 4ms ░░░░░░░░░░░░░░░░░░░░
event loop max 42ms, p99 17ms, mean 4.3ms
modules 312 total, 59 cached
time split 286ms first‑party, 503ms node_modules
트리는 부모 → 자식 로드 관계를 포함 타이밍(전체 서브트리가 걸린 시간)과 심각도에 따라 색칠된 막대 차트로 보여줍니다. 한눈에 다음을 알 수 있습니다:
sequelize가 문제입니다.sequelize내부에서pg와lodash가 대부분의 작업을 수행하고 있습니다.
내부 작동 방식
핵심 기법은 간단합니다 — coldstart가 Module._load를 몽키패치합니다 (Node가 모든 require() 호출 시 내부적으로 사용하는 함수).
- 원본
_load가 실행되기 전에performance.now()와 부모 모듈을 기록합니다. - Node가 작업을 수행하도록 둡니다 — 모듈을 해석하고, 컴파일하고, 실행합니다.
_load가 반환된 후 종료 시간을 기록합니다.
원시 이벤트는 다음과 같습니다:
{
"request": "...",
"resolvedPath": "...",
"parentPath": "...",
"startMs": 123.45,
"endMs": 124.67,
"cached": false
}
ESM의 경우, Node의 module.register() 로더 훅( Node 18.19+에서 사용 가능)을 이용해 resolve와 load 이벤트를 포착하고, 타이밍 데이터를 메시지 채널을 통해 메인 트레이서로 전달합니다.
앱이 시작을 마치면 트레이서는 다음을 구축합니다:
- 트리 – 런타임에 로드된 실제 부모 → 자식 의존성 그래프.
- 포함 시간 – 모듈 및 그 모듈이 가져온 모든 하위 모듈의 전체 실시간(벽시계) 시간.
- 배제 시간 – 자식 모듈을 제외한, 해당 모듈 자체 초기화 비용만.
- 이벤트‑루프 통계 – 시작 중 최대, 평균, p99 차단 시간(
perf_hooks사용). - 분할 – 순수 코드와
node_modules가 차지한 시간 비율.
포함 시간과 배제 시간의 구분이 핵심입니다. 포함 시간은 높지만 배제 시간이 낮은 모듈은 단순히 관문 역할을 하는 경우가 많습니다 — 무거운 자식 모듈을 불러오지만 자체적으로는 느리지 않습니다. 배제 시간이 높다는 것은 그 특정 모듈이 로드 시점에 비용이 많이 드는 작업을 수행하고 있다는 뜻입니다.
사용 방법 3가지
1. CLI (가장 쉬움 – 모든 앱을 프로파일링)
coldstart server.js
coldstart --json server.js # 기계가 읽을 수 있는 출력
coldstart -- node --inspect app.js # Node 플래그를 그대로 전달
2. 프로그래밍 API (자신의 도구에 임베드)
import { monitor, renderTextReport } from '@yetanotheraryan/coldstart'
const done = monitor()
require('./bootstrap')
require('./server')
console.log(renderTextReport(done()))
3. 프리로드 모드 (코드 변경 없이)
node --require @yetanotheraryan/coldstart/register server.js
# 또는 ESM인 경우:
node --import @yetanotheraryan/coldstart/register server.mjs
또한 renderFlamegraphHtml() 내보내기를 통해 브라우저에서 열 수 있는 독립형 HTML 플레임그래프를 생성할 수 있습니다 — 팀과 공유하거나 PR 설명에 삽입할 때 유용합니다.
실제로 작업에서 발견한 내용
Running coldstart를 우리 서비스에 적용했을 때 1초도 안 되어 원인이 명확해졌습니다: 세 단계 깊은 전이 의존성이 모듈 스코프에서 동기 파일 I/O를 수행해 설정 파일을 읽고 있었습니다. 의존성 버전 업데이트가 초기화 경로를 바꿨습니다.
해결책은 한 줄짜리 지연 require()를 사용해 로드를 중요한 시작 경로 밖으로 옮기는 것이었습니다. 부팅 시간이 ~320 ms로 돌아왔습니다.
완전한 투명성
저는 이를 구축하는 동안 Claude를 많이 사용했습니다 — ESM 로더 훅을 스캐폴딩하고, flamegraph HTML 템플릿을 생성하며, 트리‑렌더링 로직을 반복해서 다듬는 데 활용했습니다. 핵심 아이디어(performance.now() 책갈피로 Module._load를 패치하는)와 전체 아키텍처는 제가 직접 만든 것이지만, AI가 구현을 가속화했습니다. 지금은 많은 솔로 오픈‑소스가 이렇게 만들어지고 있다고 생각하며, 저는 이를 솔직히 밝히고 싶습니다.
왜 --cpu-prof만 사용하지 않나요?
--cpu-prof는 어떤 코드가 실행되고 있는지 이해하는 데는 훌륭하지만 어떤 모듈 로드가 느린지 혹은 우리를 여기까지 이끈 의존성 체인을 알려주지는 못합니다.
V8 내부와 함수 호출에 대한 플레임그래프는 얻을 수 있지만, 타이밍이 포함된 require() 트리 맵은 얻을 수 없습니다.
coldstart는 의도적으로 더 높은 수준에서 동작합니다. 그것은 “어떤 npm 패키지가 내 시작을 느리게 만들고 있나요?” 라는 질문에 답하고, “어떤 V8 내장 함수가 뜨거운가?” 라는 질문에는 답하지 않습니다.
두 도구는 상호 보완적입니다:
- coldstart를 사용해 느린 모듈을 찾습니다.
- 그 모듈이 왜 느린지 이해하려면
--cpu-prof를 사용합니다.
현재 상황 및 부족한 점
현재 구현된 기능
- CommonJS 프로파일링
- ESM 프로파일링 (Node 18.19+)
- CLI, 프로그래밍 API, 프리로드 모드
- 텍스트 보고서, JSON 보고서, HTML 플레임그래프
아직 구현되지 않음
- 동적
import()추적 - 시작 최적화를 위한 워치 모드
- CI 통합 (시작 시간이 임계값을 초과하면 실패)
아직 초기 단계입니다. API는 일상적인 사용에 충분히 안정적이지만, 출력 형식을 계속 다듬고 있으며 실제로 필요한 몇 가지 기능을 고려하고 있습니다.
사용해 보기
npm install @yetanotheraryan/coldstart
또는 npx 로 한 번 실행해 보세요:
npx @yetanotheraryan/coldstart your-app.js
GitHub:
If this is useful to you, a star on the repo genuinely helps with discoverability. And if you run it on your app and find something interesting — I’d love to hear about it in the comments. What was your slowest module?
저는 Aryan입니다 — 부업으로 Node.js용 오픈‑소스 도구를 만들고 있습니다. 다른 프로젝트는 GitHub에서 확인하실 수 있습니다.