- Đội GitHub rewrite kiến trúc React của tab "Files changed", hạ INP từ ~450ms xuống ~100ms, cắt 74% component render, giảm 50% memory.
- Với PR p95+, TanStack Virtual kéo INP từ 275–700ms về 40–80ms.
- Đây là case study hiếm về perf ở scale thật.
TL;DR
GitHub rewrite toàn bộ tab Files changed trong pull request bằng React v2, chính thức bật mặc định cho mọi user từ 22/01/2026. Kết quả đo trên PR 10.000 dòng diff (M1 MacBook Pro, CPU slowdown 4×): INP giảm từ ~450ms xuống ~100ms (≈78% nhanh hơn), memory giảm ~50%, số component render giảm 74%. Với những PR khổng lồ p95+ (>10k dòng), GitHub tích hợp TanStack Virtual, kéo INP từ 275–700ms về 40–80ms và cắt 10× JavaScript heap.
Chuyện gì đang xảy ra
Trước v2, experience của trang Files changed trở nên không thể dùng được với các PR lớn. GitHub đo được JavaScript heap vượt 1 GB, DOM node vượt 400.000, và input lag cảm nhận được bằng tay. Đội engineering ở GitHub mất nhiều quý để đại tu hạ tầng React bên dưới — và bài viết "The uphill climb of making diff lines performant" trên GitHub Blog kể lại chi tiết những kỹ thuật họ dùng.
Điểm hay: phần lớn kỹ thuật là principle cơ bản, không phải thủ thuật độc quyền. Chúng áp dụng được cho bất kỳ React codebase nào đang render list lớn.
Vì sao nó quan trọng
INP (Interaction to Next Paint) là Core Web Vital mới thay thế FID từ tháng 3/2024. Google coi INP > 200ms là poor. Một tab quan trọng bậc nhất của developer workflow — nơi bạn review code hằng ngày — trước đây thường xuyên vượt ngưỡng này. Việc một sản phẩm scale như GitHub công khai số liệu trước/sau + kiến trúc là tài liệu reference hiếm hoi cho team nào đang build UI dense + interactive.
Số liệu kỹ thuật
Benchmark của GitHub (split diff, PR 10.000 dòng, M1 + 4× CPU slowdown):
| Metric | v1 | v2 | Cải thiện |
|---|---|---|---|
| Lines of code | 2.800 | 2.000 | 27% ít hơn |
| Unique component types | 19 | 10 | 47% ít hơn |
| Components rendered | ~183.504 | ~50.004 | 74% ít hơn |
| DOM nodes | ~200.000 | ~180.000 | 10% ít hơn |
| Memory | ~150–250 MB | ~80–120 MB | ~50% ít hơn |
| INP | ~450 ms | ~100 ms | ~78% nhanh hơn |
Với PR p95+ dùng window virtualization: 10× giảm JS heap và DOM nodes, INP 275–700ms → 40–80ms.
Cách họ làm
1. Từ 8 component/dòng xuống 2
Ở v1, mỗi dòng unified diff cần tối thiểu 8 React component, split view cần tối thiểu 13. Phần lớn là thin wrapper dùng để share code giữa Split và Unified view. Mỗi wrapper lại cõng logic cho cả hai view dù chỉ một view được render. V2 tách đôi — mỗi view có component riêng. Code trùng lặp một ít, nhưng đơn giản hơn và nhanh hơn hẳn.
2. Một event handler top-level thay vì n handler
Thay vì mỗi dòng tự đăng ký onMouseEnter, v2 dùng một handler duy nhất ở tầng cao nhất, đọc data-attribute của target. Ví dụ click-drag chọn nhiều dòng: handler đọc data-attribute của từng event để biết highlight dòng nào. Cắt được hàng nghìn closure/listener trên PR lớn.
3. O(1) data access với JavaScript Map
V1 tích tụ nhiều lookup O(n) xuyên khắp shared store. V2 redesign state machine dùng Map. Access pattern đơn giản:
commentsMap['path/to/file.tsx']['L8']Mỗi dòng diff kiểm tra comment của chính nó bằng 1 lần tra key — hằng số, không phụ thuộc độ lớn PR.
4. Kỷ luật với useEffect
useEffect bị giới hạn chỉ ở top level của diff file. Đội GitHub thêm lint rule cấm dùng hook này trong các component bọc từng dòng. Hệ quả: memoization hoạt động đúng, behavior predictable, bớt re-render rác.
5. Localized state cho comment/context menu
State phức tạp (UI comment, context menu) được đẩy xuống child component conditionally rendered. Lý do: chỉ một phần nhỏ dòng có comment, không cần mọi dòng cõng state đó. Diff-line component chỉ lo render code — Single Responsibility Principle đúng nghĩa.
6. Window virtualization với TanStack Virtual
Với PR p95+ (>10k dòng diff), kể cả component siêu nhẹ cũng không cứu nổi. GitHub tích hợp TanStack Virtual — chỉ render phần diff đang hiển thị, swap element khi scroll. 10× giảm heap, 10× giảm DOM node.
7. Tiểu tiết cộng dồn
- Gỡ thẻ
<code>thừa trong cell số dòng → cắt 20.000 node trên diff 10k dòng. - Thay selector CSS nặng (như
:has(...)). - Drag/resize re-engineer bằng GPU transform — không còn forced layout.
- Server chỉ hydrate diff line đang visible → time-to-interactive giảm mạnh.
- Progressive diff loading + background fetch để user tương tác ngay không chờ load hết.
Bài học áp dụng được
Case study này củng cố vài bài học perf không mới nhưng thường bị quên:
- Abstraction có giá. Shared wrapper giữa hai view nghe hợp lý, nhưng ở 10k dòng nó trở thành gánh nặng. Đo trước khi DRY.
- Component count × hàng × interaction = cost thực tế. Một component thừa trên mỗi dòng = hàng chục nghìn component thừa trên PR lớn.
- Data structure quan trọng hơn bạn nghĩ. O(n) vs O(1) cho operation chạy mỗi frame là sự khác biệt giữa mượt và giật.
- Localize state theo xác suất sử dụng. Nếu chỉ 1% dòng có comment, đừng bắt 100% dòng giữ state comment.
- Lint để bảo vệ invariant. "Không
useEffecttrong line component" chỉ an toàn nếu có ESLint rule chặn. - Monitoring mảnh. INP tracking theo từng kiểu interaction + segmentation theo size PR giúp catch regression sớm.
Kết
Bạn có thể opt-out về experience cũ nếu muốn, nhưng mặc định trang mới đã bật từ 22/01/2026. Với user review PR lớn hằng ngày, cải thiện này là một trong những perf win cảm nhận rõ nhất của GitHub trong năm nay.
Nguồn: GitHub Engineering Blog, GitHub Changelog 22/01/2026.