Renderer code của Codecov bị crash với file lớn. File checker.ts của TypeScript có 52.283 dòng. Render tốn ~0.9ms mỗi dòng. Với 52K dòng, đó là 46.9 giây render — tab trình duyệt chết.
52.283 Dòngchecker.ts
×
0.9ms / dòngChi phí render
=
46.9 giâyTrang bị đóng băng
Tokenization nhanh (~90ms). Nút cổ chai là tạo 52K DOM element cùng lúc — mỗi dòng có số dòng, nội dung syntax-highlighted và dữ liệu coverage.
1
Virtual List — Chỉ Render Dòng Hiển Thị
Giải pháp cốt lõi: Chỉ render các dòng hiển thị trong viewport. Một spacer div có toàn bộ chiều cao ảo (để scrollbar hoạt động), nhưng DOM element thực sự chỉ tồn tại cho các dòng hiển thị + buffer overscan. Codecov dùng @tanstack/react-virtual — ở đây chúng ta tái hiện cùng logic trong JS thuần.
// Configuration — same params as @tanstack/react-virtualconst LINE_HEIGHT = 20;
const OVERSCAN = 45;
const totalLines = tokens.length;
Cấu trúc render dùng một spacer div với toàn bộ chiều cao ảo, và mỗi dòng hiển thị được absolute position bằng translateY:
// Container has full virtual height — scrollbar works correctlyconst spacer = document.createElement('div');
spacer.style.height = (totalLines * LINE_HEIGHT) + 'px';
spacer.style.position = 'relative';
viewport.appendChild(spacer);
const pool = newMap(); // tracks which lines are in the DOMfunctionrender() {
// Calculate which lines are visibleconst scrollTop = viewport.scrollTop;
const viewH = viewport.clientHeight;
const vStart = Math.floor(scrollTop / LINE_HEIGHT);
const vEnd = Math.ceil((scrollTop + viewH) / LINE_HEIGHT);
const rStart = Math.max(0, vStart - overscan);
const rEnd = Math.min(totalLines - 1, vEnd + overscan);
// Remove lines that scrolled out of rangefor (const [idx, el] of pool) {
if (idx < rStart || idx > rEnd) { el.remove(); pool.delete(idx); }
}
// Add lines that scrolled into rangefor (let i = rStart; i <= rEnd; i++) {
if (!pool.has(i)) {
const el = document.createElement('div');
el.style.cssText = `position:absolute;top:0;width:100%;
height:${LINE_HEIGHT}px;
transform:translateY(${i * LINE_HEIGHT}px)`;
el.innerHTML = getHTMLLine(i);
spacer.appendChild(el);
pool.set(i, el);
}
}
}
viewport.addEventListener('scroll', render, { passive: true });
render(); // initial render
Thử nghiệm: So sánh render 10.000 dòng tất cả cùng lúc vs ảo hóa.
Tổng Dòng
—
DOM Node
—
Thời Gian Render
—
Dòng Hiển Thị
—
2
Overscan = 45 — Ngăn Flash Trống
Virtualization đơn giản cho thấy khoảng trống khi cuộn nhanh. Giải pháp: render thêm 45 dòng phía trên và dưới viewport. Người dùng không bao giờ thấy khoảng trống vì buffer đã sẵn sàng trước khi họ cuộn đến đó.
Điều chỉnh overscan và cuộn viewer phía trên để cảm nhận sự khác biệt:
DOM node: —
Hiển thị: —
Overscan buffer: —
3
Z-Index Layering — Textarea + Số Dòng + Code
Virtualization làm hỏng tính năng tìm kiếm native của trình duyệt (Ctrl+F) vì nội dung DOM không hiển thị không tồn tại. Giải pháp của Codecov: overlay một textarea trong suốt chứa toàn bộ nội dung file. Trình duyệt tìm kiếm trong text của textarea, và người dùng thấy code syntax-highlighted phía sau.
z-index: 3Số Dòng
Render ảo, click để điều hướng hash
z-index: 2Textarea
Toàn bộ nội dung, color: transparent, bật tìm kiếm Ctrl+F
Thử nghiệm: Nhấn Ctrl+F và tìm text trong demo đầy đủ bên dưới — tính năng tìm kiếm native của trình duyệt hoạt động dù hầu hết dòng không có trong DOM, vì textarea chứa toàn bộ text.
4
Đồng Bộ Cuộn Ngang
Vì người dùng tương tác với textarea trong suốt (z-index 2), cuộn ngang xảy ra trên textarea. Phần hiển thị code (z-index 1) phải theo. Đồng bộ scrollLeft từ textarea đến code overlay:
// Sync horizontal scroll: textarea → code display overlay
textarea.addEventListener('scroll', () => {
codeOverlay.scrollLeft = textarea.scrollLeft;
updateHScrollbar();
}, { passive: true });
// Width sync — all code lines share the same widthfunctionsyncWidth() {
const w = Math.max(textarea.scrollWidth, viewport.clientWidth - 56);
codeOverlay.style.width = (viewport.clientWidth - 56) + 'px';
for (const [, el] of codeLinePool) {
el.style.minWidth = w + 'px';
}
}
Họ cũng cần đồng bộ chiều rộng — tất cả dòng code phải có cùng chiều rộng (đo từ scrollWidth của textarea) để tránh không nhất quán khi cuộn ngang.
5
Tối Ưu Pointer Events
Khi cuộn, trình duyệt kiểm tra những gì dưới con trỏ để xử lý hover — gây ra forced reflow mỗi frame. Giải pháp: đặt pointerEvents: 'none' khi cuộn, reset về 'auto' sau 50ms nhàn rỗi.
// CSS: .scrolling .virt-code-line { pointer-events: none; }// In the render() function, called on every scroll:
viewport.classList.add('scrolling'); // disable pointer-events via CSSclearTimeout(state.scrollTimer);
state.scrollTimer = setTimeout(() => {
viewport.classList.remove('scrolling'); // re-enable after 50ms idle
}, 50);
Cuộn demo bên dưới — quan sát log để thấy pointer-events bật/tắt:
6
Cuộn Đến Dòng qua URL Hash
Codecov lưu dòng đích trong URL hash (ví dụ #L500) và cuộn đến dòng đó khi tải trang. Một flag ngăn chạy nhiều hơn một lần:
// Select line — updates URL hash and highlightsfunctionselectLine(idx) {
state.selectedLine = idx;
history.replaceState(null, '', '#L' + (idx + 1));
// Toggle .selected / .highlighted on pool elementsfor (const [i, el] of lineNumPool)
el.classList.toggle('selected', i === idx);
for (const [i, el] of codeLinePool)
el.classList.toggle('highlighted', i === idx);
}
// Scroll to linefunctionscrollToLine(n) {
viewport.scrollTop = Math.max(0, n - 1) * LINE_HEIGHT;
selectLine(n - 1);
}
// Read URL hash on initfunctionreadHashOnInit() {
const match = location.hash.match(/^#L(\d+)$/);
if (match) {
const line = parseInt(match[1]);
if (line > 0 && line <= totalLines) scrollToLine(line);
}
}
readHashOnInit();
Thử nghiệm: Click vào số dòng trong demo, hoặc dùng nút jump. URL hash cập nhật và có thể chia sẻ.
✓
Demo Đầy Đủ — Tất Cả Giải Pháp Kết Hợp
50.000 dòng với đủ 6 kỹ thuật: virtual list, overscan 45, z-index layering với textarea cho Ctrl+F, đồng bộ cuộn ngang, tối ưu pointer-events và URL hash cuộn-đến-dòng.
Tổng Dòng
—
DOM Node
—
Thời Gian Khởi Tạo
—
Dòng Đã Chọn
—
✓
Điểm Cốt Lõi
Ảo hóa list — Dùng absolute positioning + translateY. DOM giữ ~110 node bất kể kích thước file.
Overscan = 45 — Các dòng thêm phía trên/dưới viewport ngăn flash trống khi cuộn nhanh.
Textarea overlay trong suốt — Giữ nguyên tính năng tìm kiếm Ctrl+F native. z-index: 2, color: transparent, white-space: pre.
Z-index layering — Số dòng (z:3) trên textarea (z:2) trên hiển thị code (z:1). Mỗi layer phục vụ mục đích tương tác cụ thể.
Đồng bộ cuộn ngang — Lắng nghe sự kiện scroll của textarea, copy scrollLeft sang code overlay. Dùng { passive: true }.
Tắt pointer-events khi cuộn — Ngăn forced reflow. Bật lại sau 50ms nhàn rỗi.
URL hash để điều hướng dòng — Đặt scrollTop = line * LINE_HEIGHT khi tải, với flag chỉ chạy một lần.