고성능 Diff 렌더링 뒤에 숨겨진 엔지니어링
풀 리퀘스트(pull request)를 열 때, 변경 사항이 방대해지기 전까지는 보통 매끄러운 경험을 제공합니다. 에이전트가 생성한 구현체든, 방대한 리팩토링이든, 리뷰 영역은 종종 성능이 저하됩니다. 파일 로딩이 느려지고, 탐색이 버벅거리며, 브라우저가 힘겨워하기 시작합니다. 대부분의 도구에서 diff 렌더링은 제품의 핵심 기능이 아니라 단순한 유틸리티에 불과합니다. 하지만 규모가 수천 개의 파일이나 수백만 줄에 이르면, 렌더링은 전체 리뷰 워크플로우의 주요 병목 현상이 됩니다.
이를 해결하기 위해 CodeView (Diffs 프로젝트의 일부) 팀은 야심 찬 목표를 세웠습니다. 규모에 관계없이 브라우저에서 거의 모든 diff를 즉각적으로 렌더링할 수 있도록 만드는 것입니다. 이를 달성하기 위해서는 브라우저가 DOM 복잡성, 메모리, 프로세싱을 처리하는 방식에 대한 근본적인 재고가 필요했습니다.
규모에 따른 핵심 과제
Diff를 렌더링하는 것은 단순히 텍스트를 표시하는 것 이상의 작업입니다. 전문적인 리뷰 영역은 구문 강조(syntax highlighting), 줄 번호, 주석, 분할/통합 레이아웃, 테마를 필요로 합니다. 이러한 각 기능은 diff의 크기에 따라 선형적으로, 혹은 그보다 더 심하게 복잡성이 증가합니다. 이러한 과제는 세 가지 주요 범주로 나뉩니다:
- 렌더링: DOM 복잡성이 급격히 증가하여 스크롤 중에 브라우저에 과부하를 줍니다.
- 프로세싱: 단일 파일에서는 빠른 작업이 수천 번 반복될 때 비용이 많이 들게 됩니다.
- 메모리: 렌더링 데이터 구조로 변환된 대규모 diff는 브라우저 메모리 제한을 초과하고 빈번한 가비지 컬렉션(GC)을 유발할 수 있습니다.
렌더링 문제 해결: Inverse Sticky 기술
가상화(Virtualization 또는 windowing)는 뷰포트 근처의 콘텐츠만 렌더링하여 DOM 크기를 작게 유지하는 표준적인 접근 방식입니다. 그러나 표준 가상화는 사용자가 JavaScript가 새로운 콘텐츠를를 렌더링하는 속도보다 빠르게 스크롤할 때 빈 공간이 나타나는 "blanking" 현상으로 고통받는 경우가 많습니다.
이를 방지하기 위해 CodeView는 Inverse Sticky 기술이라 불리는 하이브리드 접근 방식을 사용합니다.
전통적인 sticky positioning에서는 요소(예: 헤더)가 스크롤을 지나칠 때 뷰포트 상단에 머뭅니다. Inverse Sticky 기술은 이 로직을 뒤집습니다. 스크롤을 내릴 때는 렌더링된 영역의 하단 가장자리가 뷰포트 하단에 붙고, 스크롤을 올릴 때는 상단 가장자리가 뷰포트 상단에 붙습니다.
음수 top 및 bottom sticky offset—(contentHeight - viewportHeight) * -1로 계산됨—을 사용함으로써, 시스템은 네이티브 브라우저 스크롤을 유지하면서 렌더링된 영역이 완전히 스크롤되어 사라지지 않도록 보장합니다. 이는 대규모 스크롤바 점프 중에도 blanking 현상을 효과적으로 제거합니다. JavaScript가 렌더링 범위를 업데이트할 수 있을 때까지 콘텐츠가 뷰포트 가장자리에 "붙어" 있기 때문입니다.
확장 가능한 레이아웃 및 스크롤 앵커링
가상화는 높이 추정치가 정확할수록 효과적입니다. 가상화 도구가 파일의 높이를 잘못 추정하면 스크롤바가 튀고 뷰가 버벅거립니다.
CodeView는 2단계 시스템을 사용합니다. 첫 번째 단계는 저렴한 추정치입니다: (lineHeight * totalLines) + (hunkSeparatorHeight * hunkCount). 대규모 파일에서 줄 범위를 찾는 작업을 최적화하기 위해, 팀은 캐시된 "position to line" 체크포인트 시스템을 구현하여, 0번 줄부터 반복하는 대신 이진 탐색을 통해 렌더링 범위의 시작점을 찾을 수 있도록 했습니다.
뷰를 안정적으로 유지하기 위해, 팀은 브라우저의 네이티브 스크롤 앵커링(overflow-anchor: none)을 비활성화하고 커스텀 솔루션을 구현했습니다. 첫 번째로 완전히 보이는 줄이나 파일을 앵커로 추적함으로써, CodeView는 높이 변화를 조정하고 스크롤 위치를 수동으로 조정하여, DOM이 업데이트되는 동안에도 사용자의 포커스가 안정적으로 유지되도록 합니다.
극한의 사례를 위한 메모리 최적화
Linux v6.0과 v7.0 사이의 diff와 같은 대규모 데이터셋을 대상으로 테스트를 진행한 결과, 심각한 메모리 병목 현상이 발견되었습니다.
파싱된 문자열 분리(Detaching Parsed Strings)
JavaScript에서 서브스트링(substrings)은 때때로 원래의 부모 문자열에 대한 참조를 유지할 수 있습니다. 700MB 패치 파일을 파싱할 때, 줄 콘텐츠의 작은 서브스트링을 유지하는 것은 의도치 않게 전체 원본 문자열을 메모리에 계속 살려둘 수 있습니다. 문자열을 명시적으로 복사하여 소스에서 분리함으로써, 팀은 Linux diff의 메모리 사용량을 2.4 GB에서 1.15 GB로 줄였고 파싱 시간을 80% 개선했습니다.
DOM 풀링 및 공유 상태
격렬한 스크롤을 격렬하게 수행할 때 가비지 컬렉션 일시 중지(GC pause)를 줄이기 위해, CodeView는 DOM 풀링을 구현합니다. 뷰포트 영역에 들어오는 모든 파일에 대해 Shadow DOM 래퍼(스타일시트와 SVG 아이콘을 포함하는)를 파괴하고 다시 생성하는 대신, 시스템은 이러한 셸(shell)을 재사용하고 내부 콘텐츠만 교체합니다.
또한, 팀은 설정 상태를 최적화했습니다. 원래는 모든 파일이 각자의 options 객체를 가지고 있었습니다. 수만 개의 파일에 대해, 단일 설정(예: split 뷰에서 unified 뷰로 전환)을 변경하는 것은 모든 인스턴스를 반복하며 업데이트해야 함을 의미했습니다. 상태를 CodeView의 공유된 단일 진실의 원천(source of truth)으로 이동하고 아이템에 특기화된 getter를 사용함으로써, 시각적 변경 사항은 전체 리뷰를 다시 작성하지 않고도 즉각적으로 일어납니다.
지연된 프로세싱(Deferred Processing)
구문 강조(syntax highlighting)는 계산 비용이 가장 많이 드는 작업 중 하나입니다. 메인 스레드를 차단하지 않기 위해, CodeView는 워커 풀(worker pool)을 사용하여 강조 작업을 지연시킵니다.
즉시 렌더링: 파일은 즉시 읽을 수 있도록 먼저 일반 텍스트로 렌더링됩니다.
Asynchronous Highlighting: 워커 풀에서 실행되는 Shiki가 백그라운드에서 강조 작업을 프로세싱합니다.
LRU Caching: 결과는 최근 사용되지 않은 항목을 제거하는 LRU 캐시(Least Recently Used cache)에 저장되어, 다시 뷰포트로 들어오는 코드를 재처리하지 않도록 합니다.
비판적 성찰 및 트레이드오프
이러한 성과에도 불구하고, 팀은 브라우저가 이러한 수준의 데이터를 처리하기에 항상 이상적인 환경은 아니라는 점을 인정합니다. 남아있는 과제는 다음과 같습니다:
- CSS Bottlenecks: 격렬한 스크롤할 때 레이아웃 및 페인트 비용이 주요 오버헤드로 남습니다.
- Serialization Overhead: 수만 줄의 강조된 데이터를 워커와 메인 스레드 사이에서 주고받는 것은 병목 현상이 될 수 있습니다.
- Horizontal Scale: 수직적 가상화는 해결되었지만, 매우 긴 줄(예: minified JS)은 여전히 상당한 DOM 히트(DOM hit)를 유합니다합니다.
커뮤니티 관점에서는, 일부 개발자들은 가상화가 불필히히는 복잡성을 추가한다고 주장하며 현대적 하드웨어는 대규모 diff 렌더링을 네이티브하게 처리할 수 있어야 한다고 말합니다. 다른 이들은 대규모 diff의 진짜 문제는 컴퓨터 성능이 아닌 인간의 인지 능력이라고 지며, AST-based diffing이나 세만틱 분석이 단순한 렌더링 속도보다 더 가치치않습니다. 0
궁극적으로, 이 프로젝트는 브라우저의 한계를 시험하며, WebKit과 Chromium을 엔진 수준에서 밀어붙여 도구가 워크플로우를 방해하는 것이 아니라 지원하도록 만드는 데 목적이 있습니다.