diff 라인을 고성능으로 만드는 험난한 여정
핵심 내용
GitHub는 대형 PR에서 1GB heap과 40만 DOM 노드를 줄이기 위해 diff 라인을 재설계했다.
자세히 보기
GitHub는 Files changed 탭을 React 기반으로 새로 만들며, 대형 pull request에서도 빠르고 안정적인 리뷰 경험을 유지하는 데 집중했다. 최악의 경우 JavaScript heap 1GB 초과, DOM 노드 40만 개 이상, 느린 상호작용과 높은 INP가 문제가 됐다.
핵심 해법은 하나가 아니라 여러 갈래의 최적화였다.
- v1 diff 라인 구조 단순화: unified view는 라인당 약 10개 DOM 요소, split view는 약 15개 DOM 요소를 쓰고, React 레벨에서도 unified는 최소 8개 컴포넌트, split은 최소 13개 컴포넌트가 필요했다.
- 컴포넌트 구조 축소: v2에서는 split/unified를 위한 전용 컴포넌트를 분리해 중첩 wrapper를 줄였고, 한 diff line이 8개 컴포넌트에서 2개 수준으로 줄었다.
- 이벤트 핸들링 단순화: 여러 하위 컴포넌트에 흩어져 있던 이벤트 대신, 상위의 단일 handler가
data-attribute를 보고 처리하도록 바꿨다. - 상태 이동과 lookup 최적화: 댓글과 context menu 상태를 조건부 렌더링 자식 컴포넌트로 옮기고,
useEffect사용을 상단으로 제한했으며,Map기반의 O(1) 조회 구조로 바꿨다.
작은 변경도 누적 효과가 컸다. 예를 들어 line number 셀에서 불필요한 <code> 태그를 제거해 10,000줄 기준 20,000개 DOM 노드를 덜 만들었다. 같은 테스트에서 v1 대비 v2는 총 컴포넌트 렌더링 ~183,504개 → ~50,004개, 메모리 ~150-250MB → ~80-120MB, INP ~450ms → ~100ms로 개선됐다.
가장 큰 PR에는 window virtualization을 도입했다. TanStack Virtual로 화면에 보이는 부분만 DOM에 유지해, p95+ pull request에서 JavaScript heap과 DOM 노드가 10배 감소했고 INP는 275–700+ ms에서 40–80 ms로 낮아졌다.
그 외에도 렌더링 리렌더링 축소, :has(...) 같은 무거운 CSS 선택자 제거, 드래그/리사이즈의 GPU transform 전환, interaction-level INP 추적과 diff-size 세분화, memory tagging을 포함한 Datadog 대시보드, 보이는 diff line만 hydrate하는 서버 렌더링 개선, progressive diff loading과 백그라운드 fetch까지 더해져 대형 PR의 응답성과 메모리 압박을 전반적으로 낮췄다.
이 한국어 요약은 AI가 자동으로 만들었습니다. 원문의 주장과 맥락은 원문에서 확인해 주세요. 저작권은 원저작자에게 있습니다.