GitHub Blog
Điểm AI 49/100

Sản phẩm

Cách GitHub Copilot tối ưu hóa hiển thị các Pull Request khổng lồ

(giờ Việt Nam)

Tóm tắt AI

GitHub Copilot đã tái cấu trúc giao diện Pull Request để xử lý các thay đổi lớn với hàng triệu dòng code. Bằng cách tách biệt hình học của mã nguồn và các khối bình luận động, ứng dụng giúp loại bỏ tình trạng giật lag khi cuộn trang trên các PR phức tạp.

Chính văn · Bản dịch AI

Rendering huge pull requests in the GitHub Copilot app

Cách chúng tôi xây dựng lại giao diện diff trong ứng dụng GitHub Copilot để mở một pull request dài hàng triệu dòng với hàng trăm bình luận đánh giá nội tuyến.

23 tháng 9, 2026

14 phút

  • Chia sẻ:

Các đợt tái cấu trúc và di chuyển mã nguồn quy mô lớn thường phải được thực hiện trong một thay đổi duy nhất.

Stacked pull requests là một cách tuyệt vời để chia nhỏ công việc thành các thay đổi nhỏ hơn, giúp việc đánh giá dễ dàng hơn và hỗ trợ các nhóm phát hành sản phẩm với ít rủi ro hơn. Tuy nhiên, một số thay đổi, giống như trường hợp này, không thể chia nhỏ một cách gọn gàng. Điều đó khiến bạn phải đối mặt với một pull request duy nhất có thể trở nên rất lớn, và các cuộc thảo luận đánh giá lại càng làm nó phình to hơn.

Trải nghiệm đánh giá cần phải duy trì sự nhanh chóng và mượt mà ngay cả khi phần diff và các cuộc thảo luận đi kèm trở nên khổng lồ. Trong GitHub Copilot app, chúng tôi đã xây dựng lại giao diện pull request với yêu cầu đó làm trọng tâm.

Để kiểm chứng mức độ hiệu quả, chúng tôi đã mở pull request lớn nhất mà mình có thể tìm thấy: một dự án mã nguồn mở với 2.200 tệp, hơn một triệu dòng thay đổi và hơn 400 bình luận đánh giá nội tuyến. Dưới đây là cách chúng tôi tối ưu hiệu năng cho ngay cả pull request cực đoan này.

Nội dung media · xem tại bài gốcMở bài gốc ↗

Phạm vi của vấn đề

Việc hiển thị một diff lớn với tốc độ cao là điều đã được hiểu rõ: ảo hóa các hàng, giữ cho DOM được gắn kết ở mức tối thiểu và tận dụng thực tế rằng mỗi hàng là một dòng mã với chiều cao đã biết.

Bình luận mới là phần khó. Chiều cao của một bình luận phụ thuộc vào cách markdown xuống dòng, các phần có thể mở rộng, liệu có khung trả lời bên trong hay không và liệu hình ảnh của nó đã tải xong chưa. Bạn chỉ có thể biết tất cả những điều đó tại thời điểm hiển thị (render). Điều này buộc phải có một kiến trúc khác.

Ba vấn đề:

  1. Đo lường. Bạn không thể biết một bình luận cao bao nhiêu cho đến khi hiển thị nó. Điều này phá vỡ thiết kế cho phép các diff lớn duy trì độ phản hồi khi bạn cuộn trang.
  2. Luồng dữ liệu. Một giao diện diff nhanh sẽ vô giá trị nếu luồng dữ liệu cung cấp cho nó bị đình trệ, hoặc nếu nó loại bỏ các công việc đã thực hiện trước đó.
  3. Cách chúng tôi thực sự tìm ra các lỗi. Những vấn đề này xuất hiện dưới tải trọng cao, trên một engine cụ thể, tại một vị trí cuộn cụ thể. Vì vậy, chúng tôi đã định nghĩa thế nào là trạng thái ổn định, đo đạc giao diện để trả lời câu hỏi đó và chạy toàn bộ vòng lặp thay đổi → đo lường → cải thiện một cách tự động.

Bước đầu tiên là hiểu về hình học giúp cho một diff chỉ chứa mã nguồn trở nên nhanh chóng. Một khi các bình luận xuất hiện, hình học đó không còn đủ nữa.

Điều gì làm cho các diff lớn trở nên nhanh chóng

Bạn không thể đặt một triệu nút DOM trên một trang. Câu trả lời tiêu chuẩn là ảo hóa: chỉ gắn kết các hàng đang hiển thị trên màn hình, cộng thêm một biên nhỏ, và tái sử dụng chính các phần tử DOM đó khi người dùng cuộn trang. Danh sách hoạt động như thể tất cả một triệu hàng đều tồn tại. Thanh cuộn có kích thước chính xác, tính năng cuộn đến hàng cụ thể hoạt động bình thường. Nhưng chỉ có khoảng 100 hàng thực sự tồn tại tại một thời điểm.

Để ảo giác này duy trì, cần có thứ gì đó cung cấp thông tin hình học. Chiều cao thanh cuộn là tổng chiều cao của tất cả các hàng. Vị trí của hàng N là tổng chiều cao của các hàng phía trên nó. Nhảy đến một hàng, vẽ thanh cuộn, quyết định cái gì hiển thị trên màn hình, tất cả đều là phép tính số học trên một bảng chiều cao. Bạn có thể xây dựng bảng đó từ các ước tính và sửa lại khi các hàng được đo lường, và các bộ ảo hóa chiều cao biến đổi đa năng làm chính xác điều đó.

Nhưng nếu mỗi hàng là một dòng mã với kích thước phông chữ đã biết, bạn không cần phải làm vậy. Bạn có thể tính toán toàn bộ bảng ngay từ đầu và nó không bao giờ thay đổi, vì vậy không có gì cần phải sửa lại sau đó.

Hãy gọi đây là hợp đồng “tất cả chiều cao được biết trước khi vẽ”. Giao diện diff của chúng tôi được xây dựng dựa trên nó:

  • Một trình kết xuất hàng mã nguồn bắt buộc, có tái sử dụng (không dùng React component cho mỗi hàng)
  • Hình học mảng định kiểu (typed-array) cho các phép tính offset
  • Các tài liệu diff do backend sở hữu được truyền tải theo cấu trúc trước
  • Một API cuộn bắt buộc với tính năng “cuộn đến hàng N” chính xác

Không có phần nào trong đó bị suy giảm hiệu năng, vì không có công việc theo khung hình (per-frame) nào tăng lên theo tổng số hàng. Đối với mã nguồn thuần túy, đây là thiết kế đúng đắn và chúng tôi đã giữ lại toàn bộ.

Bây giờ, hãy đặt một chuỗi bình luận vào giữa diff. Nó cao bao nhiêu?

Bạn không biết và không thể biết nếu không hiển thị nó. Chiều cao của nó phụ thuộc vào những thứ chỉ tồn tại tại thời điểm hiển thị và chúng có thể tiếp tục thay đổi sau lần vẽ đầu tiên:

  • Markdown xuống dòng khác nhau ở các độ rộng khác nhau
  • Các khối <details> mà người dùng có thể mở rộng hoặc thu gọn tại chỗ
  • Một trình soạn thảo trả lời mở ra bên trong chuỗi hiện tại và lớn dần khi bạn nhập liệu
  • Các diff gợi ý thay đổi, phản hồi, chế độ chỉnh sửa, các banner giải quyết vấn đề
  • Hình ảnh và các tài sản bất đồng bộ thay đổi chiều cao khi tải xong

Câu trả lời hiển nhiên là dành riêng một khe có chiều cao cố định cho mỗi bình luận, được định kích thước bởi một bộ ước tính. Nó thất bại trên một pull request lớn. Một bộ ước tính đúng về mặt trung bình vẫn sai ở các thái cực. Nó dự phòng quá mức cho hầu hết các bình luận, để lại những khoảng trắng, và dự phòng thiếu cho những bình luận phức tạp, khiến chúng bị cắt xén hoặc xuất hiện thanh cuộn lồng nhau. Nếu bạn đo chiều cao thực tế sau khi vẽ và ghi lại vào bảng offset dùng chung, mọi thứ bên dưới sẽ di chuyển trong khi người dùng đang cuộn. Đó là một cú nhảy cuộn, và trên một pull request lớn, đó là một cú nhảy rất lớn.

Vì vậy, các bình luận cần một hợp đồng khác. “Tất cả chiều cao được biết trước khi vẽ” là không thể đạt được đối với nội dung này. Thay vào đó, chúng tôi có thể cam kết: chiều cao được giới hạn, được đo lường một cách lười biếng (lazily), và các điều chỉnh là nhỏ và được neo vào bất cứ thứ gì người dùng đang xem.

Hai hệ thống hình học thay vì một

Ý tưởng giúp vấn đề này trở nên khả thi là ngừng ép buộc một hệ thống hình học phục vụ cả hai loại nội dung. Chúng tôi chia chiều cao của tài liệu thành hai miền độc lập:

total height = deterministic code height          (exact, known up front)
             + Σ dynamic block effective heights   (estimated, then measured)
             + scroll padding

Hình học mã nguồn giữ nguyên thế giới ban đầu. Nó có tính xác định, được tính tổng tiền tố, chính xác và không bao giờ bị xây dựng lại khi một bình luận thay đổi kích thước.

Hình học khối động bao phủ mọi thứ mà chúng ta không thể dự đoán chiều cao, chẳng hạn như chuỗi bình luận, bản nháp và trình soạn thảo trả lời. Mỗi khối được xác định bởi bản chất của nó thay vì vị trí hiện tại. Nó có một khóa ổn định tồn tại ngay cả khi nội dung đang tải, và được neo vào một tệp, dòng và phía thay vì tọa độ pixel, vì vậy việc reflow không thể làm mất dấu nó. Chúng tôi cũng giữ một dấu vân tay (fingerprint) của mọi thứ có thể thay đổi chiều cao của khối: nội dung của nó, liệu <details> có đang mở hay không, liệu trình soạn thảo có đang hoạt động hay không. Và chúng tôi ghi lại độ rộng mà nó được đo lần cuối, được làm tròn vào các nhóm, để việc thay đổi kích thước cửa sổ thông thường không làm mất hiệu lực mọi phép đo trong tài liệu.

Chiều cao hiệu dụng của một khối sau đó rất đơn giản: chiều cao đã đo nếu chúng ta có giá trị hợp lệ, chiều cao đã lưu trong bộ nhớ đệm nếu dấu vân tay và độ rộng vẫn khớp, và ước tính trong các trường hợp khác. Các chiều cao đó nằm trong chỉ mục riêng của chúng, tách biệt với các hàng mã nguồn, vì vậy một bình luận thay đổi kích thước không bao giờ buộc hình học mã nguồn phải xây dựng lại. Và số lượng khối được giới hạn bởi các bình luận, không phải bởi các hàng. Vài nghìn khối là ổn, miễn là lần vẽ đầu tiên không bao giờ gắn kết hoặc đo lường tất cả chúng cùng một lúc.

Bộ lập lịch đo lường và sai lầm đầu tiên của chúng tôi

Phần này mất nhiều thời gian nhất để hoàn thiện, vì thiết kế đầu tiên của chúng tôi đã sai theo một cách rất đáng học hỏi.

Cách hiển nhiên để đo lường nội dung động là sử dụng một ResizeObserver cho mỗi khối, theo dõi phần tử đó và ghi lại chiều cao đã đo vào bố cục mỗi khi nó thay đổi. Đây là phương pháp chúng tôi đã thiết kế nhưng sau đó loại bỏ trong quá trình tối ưu hóa hiệu năng. Đó là vòng lặp phản hồi mà các bề mặt ảo hóa lớn cần tránh. Một trình quan sát ghi lại chiều cao vào bố cục của chính phần tử mà nó đang theo dõi có thể tự kích hoạt lại, và chi phí sẽ tăng lên theo mỗi khối được gắn (mounted).

Thay vào đó, giải pháp được triển khai là một lượt đo lường duy nhất được kiểm soát bởi trạng thái nhàn rỗi (idle) và cuộn trang (scroll), tuân thủ cùng nguyên tắc với phía xác định (deterministic):

  • Nằm ngoài luồng xử lý chính (hot path). Nó chạy khi phạm vi hiển thị ổn định, không bao giờ chạy mỗi khung hình cuộn, và hoàn toàn chờ đợi khi thao tác cuộn đang diễn ra. Việc tính toán lại bố cục (reflow) giữa chừng khi cuộn chính là hiện tượng giật lag (jank) mà chúng tôi đang cố tránh. Nó sẽ chạy lại ngay khi thao tác cuộn dừng hẳn.
  • Giới hạn trong khung nhìn (viewport). Chỉ các khối nằm trong khoảng 2400px tính từ khung nhìn mới là ứng viên, vì vậy khối lượng công việc là O(viewport). Các khối ở xa vẫn tiếp tục sử dụng ước tính của chúng và sẽ được điều chỉnh khi tiến lại gần hơn.
  • Ưu tiên đọc dữ liệu trên màn hình. Một khối đã gắn (mounted) đang hiển thị trên màn hình, nên chiều cao hiển thị của nó là dữ liệu chính xác nhất. Lượt đo lường sẽ đọc mọi ứng viên đã gắn trong một đợt duy nhất, thực hiện một lần reflow mà không có thao tác ghi xen kẽ, sau đó ghi lại kết quả. Một khối đã gắn không bao giờ bị bỏ qua để ưu tiên cho một ước tính cũ. Quy tắc đó đã sửa được lỗi khó chịu nhất mà chúng tôi gặp phải: các bình luận hiển thị với một khoảng trắng bên dưới, do một khối đã gắn bị loại khỏi quá trình đo lường và vẫn giữ nguyên ước tính quá cao.
  • Đo lường ngoài màn hình là phương án dự phòng có giới hạn. Đối với một khối ở gần nhưng chưa gắn, lượt đo lường thực hiện tối đa một lần render ngoài màn hình để điều chỉnh phần không gian dự trữ trước khi nó cuộn vào khung nhìn. Các khối cao hơn khung nhìn thậm chí sẽ bỏ qua bước này. Phần dự trữ dư thừa của chúng nằm ẩn bên dưới màn hình, nên việc render là không cần thiết.
  • Một trình quan sát xử lý phần còn lại. Một số thay đổi về chiều cao không làm thay đổi dấu vân tay (fingerprint) và không trùng với thao tác cuộn: nhập liệu trong trình soạn thảo phản hồi, hình ảnh tải xong, hoặc đóng/mở một thẻ <details>. Mỗi khối đã gắn đều giữ một ResizeObserver, nhưng theo mặc định, nó chỉ đánh dấu khối đó để lượt đo lường nhàn rỗi đọc lại. Nó không bao giờ tự ghi chiều cao, điều vốn sẽ tạo ra vòng lặp phản hồi mà chúng tôi đã loại bỏ. Nó sẽ ngắt kết nối khi bị hủy gắn (unmount), và một tab pull request không hoạt động sẽ không quan sát bất cứ điều gì.
  • Với một ngoại lệ có chủ đích. Việc chờ đợi gây ra lỗi hiển thị rõ rệt đối với các thay đổi kích thước do chính bạn gây ra: mở rộng thẻ <details>, mở trình soạn thảo phản hồi, hoặc hình ảnh vừa tải xong. Khối đó giãn ra ngay lập tức, nhưng mã bên dưới nó chỉ di chuyển ở lượt nhàn rỗi tiếp theo. Trong một khung hình, bình luận đó cao hơn trong khi mọi thứ bên dưới vẫn ở vị trí cũ, và bạn có thể thấy rõ hai bước này. Vì vậy, khi một khối đã gắn và đang hiển thị trên màn hình, trình quan sát hiện sẽ đo và áp dụng điều chỉnh ngay trong cùng khung hình đó, trước khi vẽ (paint). Khối giãn ra, mã định vị lại, và mọi thứ bên dưới cùng dịch chuyển. Hai biện pháp bảo vệ giúp ngăn chặn việc tạo ra vòng lặp mà chúng tôi muốn tránh: tối đa một lần commit đồng bộ mỗi khung hình, giúp gộp các thay đổi kích thước dồn dập thành một, và không bao giờ thực hiện trong khi đang cuộn, nơi nó sẽ chuyển sang lượt đo lường theo đợt.

Bài gốc còn tiếp — xem tiếp tại bài gốc ↗

Bài viết được AI dịch và tổng hợp tự động từ GitHub Blog. Liên kết bài gốc ở phía trên. Dữ liệu đồng bộ qua API công khai được ghi nguồn tại AI HOT (canonical) ↗. AIHOT.vn luôn dẫn nguồn đầy đủ — nếu bạn thấy điểm cần chỉnh sửa, hãy gửi ý kiến tại trang phản hồi.

Cách GitHub Copilot tối ưu hóa hiển thị các Pull Request khổng lồ | AIHOT.vn