⚡ Electron7. 성능 & 메모리04 · V8 snapshot — 평가 시간을 미리 잘라낸다

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단계:

  1. Parse — source string → AST. 약 50ms / 1MB.
  2. Compile (Ignition) — AST → bytecode. 약 80ms / 1MB.
  3. Execute — 모든 top-level 문 실행. 클래스 정의, 모듈 import, side effect. 가장 큼.
  4. 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). 우리 앱 코드는 거기 추가하는 거다.

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-linklazy 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-cacheV8 startup snapshot
적용 난이도1줄수일~수주의 코드 정리
효과 단계parse + compileparse + compile + execute
첫 시작캐시 빌드 (느림)즉시 빠름
안정성안전환경 의존 코드에서 깨질 위험
적용 위치Main / RendererMain (주로). Renderer는 별도
효과 (대략)~30% 단축~50%+ 단축
누가 쓰는가Atom 시대 시작, 대부분의 CLIVS 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-levelsnapshot 시점 파일이 문자열로 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-cacheelectron-linkAtom 팀이 만들었다. 2016~2017년 Atom의 시작 시간이 프로젝트 인덱스가 커질수록 5초→10초로 늘어나자, Atom 코어 엔지니어 Antonio ScandurraV8 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-cacheparse + compile 캐시 (한 줄, 누구나 쓸 수 있음) → (2) electron-link + mksnapshot으로 전체 heap 직렬화 (수주의 코드 정리). 환경 의존(Date.now(), process.platform, file I/O)을 top-level에서 없애야 한다는 강한 제약이 있다. 다음: 05-gpu-and-rendering — 하드웨어 가속이 항상 정답이 아닌 이유.