⚡ Electron7. 성능 & 메모리01 · Startup time — Cold start 4단계 분해

01 · Startup time — Cold start 4단계 분해

질문: “Electron 앱이 뜨는 데 3초 걸린다” — 어디서 3초가 가는가? 한 줄 답: Cold start는 프로세스 fork → Chromium 초기화 → V8 시작 → JS 평가 4단계의 합이다. 앞의 3단계는 런타임 비용이라 우리가 못 줄이지만 — JS 평가는 절반에서 90%까지 줄어든다 (V8 snapshot, lazy require, code splitting).


Pyramid Top

브라우저는 이미 떠 있어서 사용자가 클릭하면 거의 0ms부터 시작한다. Electron은? 매번 fresh다 — Chromium 엔진을 fork하고, V8을 부팅하고, 우리 JS 수십 MB를 parse·compile·execute 한다. “왜 Slack 웹은 즉시 뜨는데 Slack 데스크톱은 1.5초 걸리나”의 답은 공정한 비교가 아니다에 있다.

그러나 정해진 비용우리 책임 비용은 다르다. 이 문서는 4단계로 분해해 어디서 시간이 가는지를 측정하고, 어느 단계가 줄어드는지를 가른다.


사고 흐름


Why — Cold start가 왜 이렇게 비싸나

① 프로세스 fork — OS와 Electron 양쪽 비용

app.getCreationTime()Electron 프로세스가 만들어진 OS 타임스탬프를 돌려준다. 그 전, OS가 바이너리를 로드하고, 메모리를 매핑하고, 동적 라이브러리(libnode, libchrome_.so)를 연결하는* 단계가 있다.

  • macOS: ~50ms (M1 SSD 기준)
  • Windows: ~100ms (NTFS + Defender 스캔 포함)
  • Linux: ~30ms

이 단계는 줄일 수 없다. 사용 가능한 우회로는 실행 자체를 미리 한다 — 트레이 아이콘 상주 (Slack·Discord 방식).

② Chromium 초기화

app.on('ready') 이전에 Chromium 내부 서브시스템들이 부팅된다 — V8 isolate 풀, network service, GPU process(별도), session, IPC channel.

  • 평균 ~300ms.
  • “Chrome 자체를 새로 띄우는” 비용이라 생각보다 크다.
  • Electron 버전이 올라가면 이 시간은 조금씩 길어진다 (보안 패치 + 새 API).

이것도 줄일 수 없다. 우리가 할 수 있는 건 측정해서 회귀를 잡는 것뿐.

③ V8 시작

JavaScript 엔진의 isolate가 생성된다 — heap, GC, JIT, builtin 함수. snapshot으로 줄일 수 있는 단계가 여기부터 시작한다.

  • 기본 V8 isolate: ~100ms
  • snapshot 적용: ~30ms (V8 자체 snapshot은 Electron 빌드에 이미 들어 있음)
  • 추가 사용자 snapshot: 우리 코드까지 미리 평가된 상태로 들어 있음 → 그만큼 ④가 작아짐.

04-v8-snapshot이 이걸 자세히.

④ JS 평가 — 우리 책임의 99%

require('./main')이 호출된 순간부터 첫 BrowserWindow.show()까지의 시간. 이 안에 전부 들어 있다:

  • require 트리 전체 (수십~수백 MB의 node_modules)
  • 우리 코드 parse + compile + execute
  • React/Vue/Angular 초기 mount
  • 첫 IPC 왕복
  • 첫 화면 fetch

이 단계가 1.5초~2초를 차지하는 게 일반적이고, 최적화의 90%가 여기서 일어난다.


How — 측정 방법

추측 금지. 수치가 먼저다.

Main 프로세스 측정

// main.js 맨 위
const startedAt = Date.now();
const procCreated = app.getCreationTime(); // OS가 프로세스를 만든 시각 (ms)
 
app.whenReady().then(() => {
  console.log('① fork → ready:', Date.now() - procCreated, 'ms');
  // = 단계 ① + ② + ③ 일부
});
 
app.on('browser-window-created', (_, win) => {
  win.webContents.once('did-finish-load', () => {
    console.log('② total to first paint:', Date.now() - procCreated, 'ms');
  });
  win.once('ready-to-show', () => {
    console.log('③ ready-to-show:', Date.now() - procCreated, 'ms');
  });
});

Renderer 측정 (performance API)

// preload.js — Renderer 시작 시점 기록
performance.mark('preload-start');
 
// renderer
performance.mark('app-mount');
performance.measure('preload-to-mount', 'preload-start', 'app-mount');
 
console.table(performance.getEntriesByType('measure'));

실측 비교 (참고치)

cold start비고
VS Code~1.2s (M1) / ~2.0s (Intel)V8 snapshot + lazy extension host
Slack~2.5s대형 React + remote auth
Discord~1.8sC++ native 모듈 다수
Notion~3-4s거의 원격 SPA + Electron 외피
빈 Electron HelloWorld~0.5s비교 baseline

이 표는 측정 방법환경에 따라 ±30%까지 흔들린다 — 절대값이 아니라 상대값으로만 본다.


What — 4단계 줄이는 표

단계우리가 할 수 있는가?기법효과
① fork거의 불가능트레이 상주, 시작 시 미리 fork (사용자 관점에선 0ms)100% (사용자 체감)
② Chromium init거의 불가능회귀 감시만0%
③ V8 시작약간startup snapshot 사용~70ms
④ JS 평가거의 전부V8 snapshot, lazy require, code splitting, async init, dynamic import50~90%

④의 구체 기법

lazy require:

// 나쁨: 시작 시 60MB의 sharp 라이브러리 로드
const sharp = require('sharp');
 
// 좋음: 실제 호출 직전에 로드
function getSharp() {
  return require('sharp'); // require 캐시 → 한 번만 평가
}

v8-compile-cache — Atom이 만든 라이브러리. require된 모든 JS의 bytecode를 캐시해 다음 시작부터는 parse·compile을 건너뛴다.

// main.js 맨 첫 줄
require('v8-compile-cache');
// ... 이후 require들이 캐시된다

Code splitting + dynamic import — Renderer 쪽 webpack/Vite에서:

// 무거운 모달은 사용자가 열 때
const heavyModal = await import('./heavy-modal.js');

ready-to-show 활용처음부터 BrowserWindow를 보이게 만들면 흰 화면이 깜빡인다. show: false로 만들고 ready-to-show에서 보여줘야 체감 속도가 올라간다.

const win = new BrowserWindow({ show: false, /* ... */ });
win.once('ready-to-show', () => win.show());

What-if — 측정 없이 최적화하면

시도결과
”node_modules 줄이면 빨라지겠지” → devDependencies 정리번들 크기는 줄지만 cold start는 거의 안 변함 (devDeps는 require되지 않음)
“Electron 버전 올리면 빨라지겠지”거꾸로 조금 느려짐 — 보안·기능 추가로 ② 비용 증가
”다 lazy require하면 빨라지겠지” → 모든 곳에 함수 래핑첫 화면 사용자가 누른 버튼이 200ms 멈춤 → 체감은 더 나빠짐
”React를 Solid로 바꾸면 빨라지겠지”mount 비용 50ms 절감. ④의 5%. 더 큰 자리(snapshot)부터 봐야.
”asar 압축 풀면 require 빨라지겠지”일부 환경에서만 사실, 일반적으로는 변함 없음 (Electron asar는 mmap)

결론: 측정 없는 최적화는 random rearrangement다. performance.mark 두 줄을 먼저.


흥미로운 이야기

VS Code의 시작 시간은 Atom의 실패가 만든 트라우마

2014~2017년경 Atom 에디터의 시작 시간은 프로젝트가 커질수록 5초, 10초로 늘어났다. 사용자들은 “Atom is slow”라는 밈을 만들었고, 같은 GitHub 출신 팀이 만든 Sublime Text (네이티브)와 비교당하며 이탈했다. 같은 시기 Microsoft가 VS Code를 완전히 같은 Electron 위에 만들면서 시작 시간을 1순위 KPI로 두었다. 그 결과물이 — V8 snapshot으로 전체 코드를 사전 컴파일, Extension Host 별도 프로세스(시작 후 lazy), lazy require 강제 — 같은 Electron인데 체감 속도가 다르다는 결과. Atom은 2022년 sunset되었고, 그 팀의 후속작 Zed는 *완전 네이티브 (Rust + GPU)*로 갔다. “Electron이 느린 게 아니라 Atom이 안 깎은 거였다” — 이 한 줄이 VS Code 팀의 자존심이자 데스크톱 웹 앱의 한 시대 교훈이다.


Insight — 시작 시간의 공정 비교

웹의 “즉시”는 브라우저를 켜는 시간이 분리되어 보이지 않는 것뿐이다. 사용자가 맥북을 막 켰을 때 브라우저까지 띄워서 Slack을 여는 시간을 합치면 — Electron Slack과 거의 같다. Electron의 cold start는 사용자가 보는 한 자리에 그 비용이 다 모인 것이다.


한 단락 요약

Electron cold start는 4단계 — fork(~50ms) → Chromium init(~300ms) → V8 시작(~150ms) → JS 평가(~1.5s). 앞의 3단계는 런타임 비용이라 우리가 못 줄이지만, JS 평가는 V8 snapshotlazy require로 50~90% 줄어든다. 최적화 전에 반드시 app.getCreationTime() + performance.mark로 측정한다 — 측정 없는 최적화는 어느 자리도 깎지 못한다. 다음: 02-memory-footprint — 시간 다음 비용 축, 메모리프로세스 수 × 단가로 본다.