Diff 라인 성능 개선을 위한 험난한 여정 (새 탭에서 열림)
GitHub는 대규모 풀 리퀘스트에서도 **Files changed** 탭의 반응성과 안정성을 유지하기 위해 단일 해결책이 아닌 여러 최적화 전략을 적용했다. 특히 diff 라인마다 생성되는 DOM 요소, React 컴포넌트, 이벤트 핸들러를 줄여 메모리 사용량과 입력 지연을 낮추고, 가장 큰 변경 사항에는 가상화를 적용해 렌더링 범위를 제한하는 방향을 택했다. 핵심 교훈은 작은 구조적 최적화도 수천 개의 diff 라인에 누적되면 큰 성능 개선으로 이어진다는 것이다. ## 대규모 풀 리퀘스트에서 성능이 어려운 이유 - GitHub의 풀 리퀘스트는 한 줄 수정부터 수천 개 파일과 수백만 줄을 포함하는 변경까지 규모 편차가 매우 크다. - 일반적인 풀 리퀘스트는 빠르게 동작했지만, 대규모 변경에서는 다음 문제가 발생했다. - JavaScript 힙이 극단적인 경우 1GB를 초과 - DOM 노드 수가 40만 개 이상으로 증가 - 페이지 상호작용이 매우 느려지거나 사실상 사용할 수 없게 됨 - 입력 후 다음 화면이 표시되기까지의 시간인 INP가 허용 수준을 초과 - 모든 기능과 브라우저의 기본 동작을 유지하면서 최악의 경우까지 해결하는 단일 기법은 현실적인 한계가 있었다. ## 풀 리퀘스트 규모별 최적화 전략 GitHub는 변경 규모와 복잡도에 따라 서로 다른 전략을 조합했다. - **diff 라인 컴포넌트 최적화** - 대부분의 풀 리퀘스트에서 기본 diff 화면을 빠르게 유지 - 중간 및 대규모 리뷰에서도 브라우저의 기본 `find-in-page` 같은 동작을 보존 - **가상화를 통한 점진적 성능 저하** - 가장 큰 풀 리퀘스트에서는 한 번에 렌더링하는 콘텐츠 양을 제한 - 모든 내용을 동시에 DOM에 올리지 않아 응답성과 안정성을 우선 - **기반 컴포넌트와 렌더링 개선** - 특정 모드에 관계없이 모든 크기의 풀 리퀘스트에 누적 효과를 제공 - 렌더링 구조 자체를 단순화해 메모리와 상호작용 비용을 줄임 ## 초기 목표와 측정 지표 최적화 작업의 목표는 단순히 평균 속도를 높이는 데 그치지 않았다. - JavaScript 힙 크기와 메모리 사용량 감소 - DOM 노드 수 감소 - 평균 INP 개선 - 특히 최악의 사용자 경험을 나타내는 p95와 p99 INP 대폭 개선 - 이를 위해 상태, HTML 요소, JavaScript 코드, React 컴포넌트 수를 전반적으로 줄이는 단순화 전략을 채택 ## v1의 문제: diff 라인당 높은 렌더링 비용 React로 처음 diff 화면을 구현한 v1은 작은 재사용 컴포넌트를 많이 조합하는 구조였다. - unified 뷰의 diff 한 줄에는 최소 약 10개의 DOM 요소가 필요했다. - split 뷰에서는 한 줄당 약 15개의 DOM 요소가 필요했다. - 구문 강조를 적용하면 추가 `<span>` 요소가 더해져 DOM 수가 증가했다. - React 계층에서도 다음과 같은 컴포넌트가 생성됐다. - unified 뷰: 한 줄당 최소 8개 컴포넌트 - split 뷰: 한 줄당 최소 13개 컴포넌트 - 댓글, hover, focus 등 추가 상태가 활성화되면 컴포넌트 수는 더 늘어났다. - 작은 컴포넌트마다 React 이벤트 핸들러를 5~6개씩 연결하는 경우가 많았다. - 결과적으로 diff 한 줄에 20개 이상의 이벤트 핸들러가 붙을 수 있었고, 이를 수천 줄에 적용하면서 비용이 급격히 커졌다. v1의 한 줄당 최소 구조는 다음과 같았다. - DOM 요소 10~15개 - React 컴포넌트 8~13개 - React 이벤트 핸들러 20개 이상 - 다수의 작은 재사용 컴포넌트 이 구조는 일반적인 규모에서는 문제가 없었지만, 데이터 크기가 사실상 제한되지 않는 대규모 풀 리퀘스트에서는 변경 줄 수가 늘수록 INP와 JavaScript 힙 사용량이 함께 악화됐다. ## v2의 방향: 작은 변경을 대규모로 누적 v2에서는 눈에 띄지 않는 HTML 구조까지 점검해 diff 라인당 비용을 줄였다. - 줄 번호 셀에 불필요하게 포함되어 있던 `<code>` 태그를 제거했다. - diff 한 줄에서 DOM 노드 2개를 줄이는 것은 개별적으로는 작은 개선처럼 보인다. - 그러나 10,000줄을 렌더링하면 DOM 노드 20,000개를 제거하는 효과가 발생한다. - 이처럼 줄 단위의 사소한 최적화도 대규모 데이터에서는 메모리 사용량과 렌더링 비용에 크게 누적된다. - 성능 개선에서는 큰 기능 변경만큼 불필요한 태그, 컴포넌트, 이벤트 핸들러를 하나씩 제거하는 작업도 중요하다. ## 실용적인 결론 대규모 목록이나 코드 diff를 렌더링할 때는 처음부터 최악의 데이터 규모를 고려해야 한다. 컴포넌트를 잘게 나누는 구조가 유지보수에는 유리할 수 있지만, 각 요소에 상태와 이벤트 핸들러를 추가하면 수천 개 항목에서 큰 비용이 된다. 따라서 DOM 구조를 단순화하고, 항목별 렌더링 비용을 측정하며, 극단적인 규모에는 가상화를 적용하는 조합이 효과적이다.