04 · V8 snapshot — 평가 시간을 미리 잘라낸다
질문: VS Code가 어떻게 같은 Electron 위에서 Atom보다 빨리 뜨나? 한 줄 답: JS 평가 비용(parse + compile + execute)을 빌드 시점에 미리 실행하고, V8 heap을 덩어리로 직렬화해 런타임 시작 시 mmap한다 — 매 시작마다 수십~수백 ms가 0이 된다.
Pyramid Top
01-startup-time에서 cold start 4단계를 분해했고, ④ JS 평가가 가장 큰 자리(1초~2초)였다. V8 snapshot은 이 자리를 빌드 시점으로 옮긴다 — 사용자 PC에서의 시간은 mmap + 일부 fixup만 남는다.
도구는 두 단계로 발전했다:
v8-compile-cache— Atom이 2016년 발명한 간이판. require된 모든 JS의 bytecode를 디스크에 캐시한다. 첫 시작은 그대로지만, 두 번째부터 30~50% 빠르다.- V8 startup snapshot — Atom·VS Code가 발전시킨 정통판. 전체 평가된 V8 heap을 직렬화해 처음부터 빠르다.
electron-link로 코드를 정리하고mksnapshot으로 만든다.
사고 흐름
Why — 왜 평가 비용이 크나
V8이 JS 한 줄을 실행하기까지 4단계:
- Parse — source string → AST. 약 50ms / 1MB.
- Compile (Ignition) — AST → bytecode. 약 80ms / 1MB.
- Execute — 모든 top-level 문 실행. 클래스 정의, 모듈 import, side effect. 가장 큼.
- Optimize (TurboFan) — 자주 호출되는 함수 JIT. (런타임 중 발생, snapshot과는 별개.)
우리 앱이 60MB의 JS를 require하면 — 1~2의 합만 600ms 이상. 3까지 합치면 1.5초.
V8 snapshot은 1~3을 빌드 머신에 옮긴다. 사용자 PC에서는 mmap + 약간의 fixup만.
How ① — v8-compile-cache (쉬운 자리)
가장 진입이 쉽다. 한 줄로 끝난다.
npm i v8-compile-cache// main.js의 *가장 첫 줄*
require('v8-compile-cache');
// 이후 require들이 자동으로 캐시된다
const { app, BrowserWindow } = require('electron');
// ...원리: require() hook을 걸어 처음 본 모듈은 V8의 cached_data를 디스크(~/Library/Caches/... 등)에 쓰고, 다음 시작부터는 그것을 읽어 vm.Script({ cachedData })로 전달한다 → parse·compile 단계 건너뛰기.
한계:
- 첫 시작은 그대로 느림. 캐시는 두 번째부터 효과.
- execute 단계는 못 건너뜀 — 그건 startup snapshot만 가능.
- 효과 ~30-50% 단축 (캐시된 시작).
누가 쓰는가: Atom (2016~), Yarn classic CLI, lerna, Storybook 일부 등. 모든 Node CLI의 기본 라이브러리에 가깝다.
How ② — V8 startup snapshot (정통)
Electron 자체가 *.bin snapshot 파일을 이미 가지고 있다 (electron/Resources/electron.asar/snapshot_blob.bin). 우리 앱 코드는 거기 추가하는 거다.
도구: electron-link + mksnapshot
npm i -D electron-link electron-mksnapshot스크립트 (개념도):
// build/make-snapshot.js
const link = require('electron-link');
const { promisify } = require('util');
(async () => {
// 1. 우리 코드의 require 그래프를 *하나의 큰 JS*로 합치기
// (단, 'snapshot 시점에 부작용을 일으키면 안 되는 모듈'은 lazy require로 변환)
const { snapshotScript } = await link({
baseDirPath: __dirname + '/..',
mainPath: __dirname + '/../app/main.js',
shouldExcludeModule(modulePath) {
// node-pty 같은 native module은 snapshot에 못 들어감
return /native|\.node$/.test(modulePath);
},
});
// 2. mksnapshot으로 V8 binary snapshot 생성
// (Electron이 쓰는 V8 버전과 정확히 일치해야 함)
// → ./snapshot_blob.bin
})();
// 3. electron-builder 등으로 이 .bin을 Electron Resources 디렉토리에 덮어쓰기
// (실제로는 'v8_context_snapshot.bin'을 교체)이게 어려운 이유:
- 같은 모듈이라도 snapshot에 들어갈 수 있는 것과 없는 것이 있다. 환경 의존(
process.platform,Date.now(), file 읽기)인 코드는 snapshot 시점에 빌드 머신 환경으로 평가되어 틀린 값으로 굳는다.electron-link가 lazy require로 자동 변환하지만 — 완벽하지 않다.- 네이티브 모듈 (
.node)은 unsnapshotable.
VS Code의 실제 구조
VS Code의 node_modules 안에는 vscode-loader라는 자체 module loader가 있다. 빌드 시 모든 우리 코드를 evaluate해서 V8 heap을 만들고, 그 heap을 snapshot으로 굳혀 배포한다. 결과:
- 시작 시간 ~40% 단축 (특히 Intel macs에서)
- 6년간(2017-2023) 점진적 적용 — 처음엔 workbench loader만, 나중에 대부분의 core 모듈까지
What — 두 가지 도구 비교
| 항목 | v8-compile-cache | V8 startup snapshot |
|---|---|---|
| 적용 난이도 | 1줄 | 수일~수주의 코드 정리 |
| 효과 단계 | parse + compile | parse + compile + execute |
| 첫 시작 | 캐시 빌드 (느림) | 즉시 빠름 |
| 안정성 | 안전 | 환경 의존 코드에서 깨질 위험 |
| 적용 위치 | Main / Renderer | Main (주로). Renderer는 별도 |
| 효과 (대략) | ~30% 단축 | ~50%+ 단축 |
| 누가 쓰는가 | Atom 시대 시작, 대부분의 CLI | VS Code, Atom 후기 |
현실 권장 순서: ①부터 시작 → 측정으로 모자라면 ②로. 작은 앱은 ①만으로 충분.
What-if — snapshot 잘못 쓰면
| 시도 | 결과 |
|---|---|
Date.now()를 모듈 top-level에 둔 채 snapshot | 사용자 PC에서 Date.now()가 빌드 시점으로 굳음 → 모든 timestamp가 과거로 |
process.platform을 조건문 없이 top-level 사용 | 빌드 머신이 macOS면 — Windows 사용자도 macOS로 인식 |
네이티브 모듈 (sharp, better-sqlite3)을 snapshot에 포함 시도 | 링크 단계에서 에러 — .node는 unsnapshotable |
fs.readFileSync('./config.json') top-level | snapshot 시점 파일이 문자열로 inline되어 굳음 → 사용자 설정 무시 |
| snapshot 만들 때 Electron mksnapshot 대신 Node mksnapshot | 절대 작동 안 함 — V8 버전 mismatch로 런타임 크래시 |
불변식: snapshot에 들어가는 코드는 순수해야 한다 (no I/O, no time, no env-dep). 그렇지 않은 건 lazy require로 분리.
흥미로운 이야기
Atom이 만든 도구를 VS Code가 정제하고, Atom이 그 정제판을 못 따라가 망했다
v8-compile-cache도electron-link도 Atom 팀이 만들었다. 2016~2017년 Atom의 시작 시간이 프로젝트 인덱스가 커질수록 5초→10초로 늘어나자, Atom 코어 엔지니어 Antonio Scandurra가 V8 snapshot을 Electron에 통합하는 PR을 올렸다. 그 PR은 Electron upstream에 merge되었고, 누구나 쓸 수 있는 공공 도구가 되었다. 그 시기 Microsoft는 VS Code를 Atom보다 빠르게 만드는 데 사활을 걸었다. 그들이 한 것은 같은 도구를 더 깊이 적용한 것 — Atom은 일부 모듈만 snapshot에 넣었지만, VS Code는 workbench 전체 + extension loader까지 넣었다. 결과: 2018년경 VS Code가 Atom보다 2배 빠름이 측정값으로 굳어졌고, 2020년 Atom의 사용자 수가 무너졌다. 2022년 GitHub은 Atom을 sunset했다. 자기가 만든 도구로 자기가 패배한 드문 사례다 — 도구의 발명만큼이나 깊이 있는 적용이 차이를 만든다.
Insight — 언제 평가하느냐의 결정
세 도구 모두 같은 작업을 한다 — JS를 평가. 언제 하느냐만 다르다. 시간은 사라지지 않고 옮겨질 뿐이다 — 사용자 시간을 빌드 머신 시간으로 옮기는 것이 snapshot의 본질이다.
한 단락 요약
V8 snapshot은 JS 평가 비용을 빌드 시점으로 옮긴다 — Atom이 발명, VS Code가 정제한 cold start 1초 단축 기법. 두 단계: (1)
v8-compile-cache로 parse + compile 캐시 (한 줄, 누구나 쓸 수 있음) → (2)electron-link+mksnapshot으로 전체 heap 직렬화 (수주의 코드 정리). 환경 의존(Date.now(),process.platform, file I/O)을 top-level에서 없애야 한다는 강한 제약이 있다. 다음: 05-gpu-and-rendering — 하드웨어 가속이 항상 정답이 아닌 이유.