Large pull requests create a problem that is easy to underestimate.

A pull request with a few hundred changed lines is usually straightforward to display. A pull request containing hundreds of thousands or even millions of changed lines is different. Loading, calculating, and rendering that much diff data can put significant pressure on both the server and the browser.

GitHub has worked on its pull request architecture to handle very large changes without requiring the browser to load and render the entire diff at once.

The interesting part is not simply that a million-line pull request can exist. The more important engineering problem is how a system can make such a large change set usable without turning the browser into a bottleneck.

This article looks at the engineering ideas behind large diff rendering, including pagination, virtualization, incremental loading, data processing, and the trade-offs involved in building a system that can handle extremely large pull requests.

Why Rendering a Huge Diff Is Difficult

A code diff is more than plain text.

For every changed file, a system may need to determine:

  • Added lines

  • Removed lines

  • Context lines

  • File names

  • Renames

  • Binary files

  • Syntax highlighting

  • Comments

  • Review threads

  • Inline suggestions

  • File-level metadata

Now imagine doing this for a very large pull request.

Suppose a pull request contains:

10,000 files
+
1,000,000 changed lines

A naive implementation could attempt to send the complete diff to the browser:

Server
  ↓
Generate complete diff
  ↓
Serialize everything
  ↓
Send huge response
  ↓
Browser parses everything
  ↓
Browser creates thousands of DOM elements
  ↓
Browser renders everything

This approach does not scale well.

The browser must allocate memory for the response, parse it, create UI elements, calculate layout, and maintain the resulting DOM.

The result can be slow loading, excessive memory usage, and poor scrolling performance.

The Key Idea - Do Not Render Everything

One of the most important principles behind scalable interfaces is simple:

If the user cannot see it, do not render it yet.

Consider a pull request with 10,000 changed files.

A developer might initially see:

Files changed

src/Orders.cs
src/Customers.cs
tests/OrdersTests.cs
...

There is no reason for the browser to create every line of every file immediately.

Instead, the application can load and render only the content required for the current view.

This general approach is commonly known as virtualization or windowed rendering.

What Is Virtualized Rendering?

Virtualized rendering means that the UI maintains only the elements needed for the visible portion of a large dataset.

Imagine a list containing:

1,000,000 rows

A browser does not need to create one million DOM nodes just because the user can eventually scroll through them.

Instead, it can render something closer to:

Rows 1 - 50

When the user scrolls:

Rows 51 - 100

replace the previous visible window.

The user experiences a continuous list, while the browser handles a much smaller number of UI elements.

The same principle can be applied to large code diffs.

Why Traditional DOM Rendering Breaks Down

Consider this simplified HTML:

<div id="diff"></div>

A naive implementation might generate an element for every line:

for (const line of millionLines) {
    const element = document.createElement("div");
    element.textContent = line;
    diff.appendChild(element);
}

This creates an enormous DOM.

The problem is not just the number of characters.

Each DOM element has overhead involving:

  • Memory

  • Layout

  • Style calculation

  • Event handling

  • Painting

  • Browser bookkeeping

A million-line diff can therefore become much more expensive than the raw text size suggests.

A Better Approach

Instead of rendering every line, render a window.

For example:

const visibleLines = lines.slice(startIndex, endIndex);

for (const line of visibleLines) {
    renderLine(line);
}

When the user scrolls, calculate a new range:

Current viewport
       ↓
Start line
       ↓
Visible lines
       ↓
End line

Only that range needs to be represented by actual UI elements.

The rest can remain as data.

Server-Side Processing Matters Too

Browser optimization is only one part of the problem.

The server also has to process the pull request.

Before a diff reaches the browser, the backend may need to:

Read repository information
        ↓
Compare revisions
        ↓
Calculate changed files
        ↓
Generate diff information
        ↓
Process metadata
        ↓
Return data to the client

For very large changes, doing all of this in one request can create excessive CPU and memory pressure.

A scalable architecture therefore tries to avoid doing unnecessary work upfront.

Incremental Loading

One approach is incremental loading.

Instead of requesting everything:

GET /pull-request/123/diff

and returning the complete million-line result, the client can request smaller pieces.

Conceptually:

Request
  ↓
File list
  ↓
Selected file
  ↓
Selected section
  ↓
Visible diff

This allows the application to delay expensive operations until the user actually needs them.

For example, if a pull request contains 5,000 changed files and the developer opens only three, there is little reason to fully render all 5,000 immediately.

Pagination and Virtualization Solve Different Problems

These concepts are related but not identical.

Technique

Main Goal

Example

Pagination

Split data into manageable requests

Load 100 files at a time

Virtualization

Limit rendered UI elements

Render only visible lines

Lazy loading

Delay loading until needed

Load a file when opened

Caching

Avoid repeated work

Reuse previously loaded diff

Streaming

Process data progressively

Display data as it arrives

A large pull request viewer may use several of these techniques together.

File-Level Virtualization

Consider a pull request with:

2,000 changed files

The file list itself can become large.

Instead of rendering all file entries, the application can virtualize the file navigation list.

For example:

Visible viewport
---------------------
File 101
File 102
File 103
File 104
File 105
---------------------

The UI maintains the correct scroll height while rendering only the visible entries.

This reduces DOM size and improves scrolling.

Line-Level Virtualization

The same idea can be applied inside a file.

Suppose one generated file contains:

200,000 lines

Rendering all 200,000 lines immediately would be expensive.

Instead, the viewer can maintain a logical position for the complete file while rendering only the portion currently visible.

Conceptually:

Logical file
0 -------------------------------- 200,000

Rendered window
       12,450 -------- 12,500

As the user scrolls, the rendered window moves.

Why Syntax Highlighting Is Expensive

Code editors and diff viewers often provide syntax highlighting.

For example:

public async Task<User?> GetUserAsync(int id)
{
    return await repository.GetByIdAsync(id);
}

A syntax highlighter may need to tokenize the code and determine which portions represent:

Keywords
Types
Methods
Strings
Comments
Operators

Doing this for one small file is inexpensive.

Doing it for millions of lines can become expensive.

A scalable implementation should avoid syntax-highlighting content that the user has not requested to view.

This is another reason lazy rendering is important.

Review Comments Add Another Layer

A modern pull request interface is not just a diff viewer.

A changed line can have:

Code
+
Review comment
+
Reply
+
Reaction
+
Suggested change
+
Resolved state

Imagine thousands of comments spread across a huge pull request.

The system has to associate comments with the correct file and line while the user scrolls through the diff.

This creates another reason to avoid building a massive DOM tree containing every review element.

Anchoring Comments to Changed Lines

A review comment is normally associated with a particular location in a diff.

Conceptually:

File: UserService.cs
Line: 42
Comment: Handle the null result

But code changes over time.

If lines are added or removed, the original position may no longer exist.

The review system therefore needs a way to associate comments with the relevant code location rather than relying only on a static screen position.

This becomes more complicated when a large pull request contains thousands of changes.

The Browser Is Often the Real Bottleneck

Developers sometimes assume that if the server can generate the data, the browser can display it.

That is not necessarily true.

Suppose the server generates:

100 MB of diff data

The browser may need to:

Download 100 MB
        ↓
Parse the response
        ↓
Create objects
        ↓
Build UI
        ↓
Apply styles
        ↓
Calculate layout
        ↓
Paint pixels

The actual memory requirement can become considerably larger than the original response.

This is why frontend architecture is critical for large code review interfaces.

A Simple Performance Model

Consider a naive diff viewer.

If the number of changed lines is N, rendering every line creates approximately:

DOM elements = N

If each line requires additional child elements for syntax highlighting, the actual element count can become:

DOM elements > N

For example:

1,000,000 lines
×
3 DOM elements per line
=
3,000,000 DOM elements

This is only an illustrative calculation, not a benchmark.

The exact number depends on the implementation.

The important lesson is that DOM complexity can grow rapidly when every piece of content becomes a separate UI element.

A Virtualized Alternative

With virtualization, suppose the viewport requires only 100 lines:

Total logical lines = 1,000,000
Rendered lines = approximately 100

The browser can maintain a logical scroll area while keeping the active DOM much smaller.

A simplified architecture looks like this:

                Million-line diff
                       |
             +---------+---------+
             |                   |
        Data model          Scroll position
                                  |
                                  ↓
                          Visible line range
                                  |
                                  ↓
                          Render small window

This architecture allows the amount of rendered UI to remain relatively stable even as the total dataset grows.

Why Large Pull Requests Are Still Difficult

Virtualization solves rendering problems, but it does not solve every problem.

A million-line pull request can still be difficult because:

Code Context Is Huge

Understanding the relationship between thousands of files is challenging.

Review Quality Can Decline

Important changes can become difficult to distinguish from generated or repetitive changes.

Navigation Becomes Harder

Finding the important part of a huge change can take significant effort.

Backend Processing Still Matters

The system still needs to calculate and organize the underlying data.

Human Review Does Not Scale Linearly

A reviewer cannot simply spend 1,000 times longer reviewing a 1,000-times-larger change.

This is one reason smaller, logically organized pull requests remain valuable.

Production Architecture for Large Diff Systems

A scalable implementation can separate responsibilities.

For example:

                 Pull Request
                      |
                      ↓
              Diff Processing
                      |
             +--------+--------+
             |                 |
         File Index        Metadata
             |                 |
             +--------+--------+
                      |
                      ↓
                API Layer
                      |
                      ↓
               Client Browser
                      |
             +--------+--------+
             |                 |
        File List          Diff Viewer
             |                 |
             +--------+--------+
                      |
                Virtualized UI

The important design principle is to avoid forcing every layer to process everything immediately.

Caching Can Reduce Repeated Work

Large pull requests can involve repeated access to the same information.

For example, a developer may:

Open file
↓
Leave file
↓
Return to file

If the system recalculates everything each time, resources are wasted.

Caching can allow previously processed data to be reused.

Possible cache targets include:

File metadata
Diff segments
Syntax information
Review metadata
Repository information

Caching strategy depends on the system and consistency requirements.

Handling Generated Files

Generated files should receive special treatment in large repositories.

For example:

GeneratedClient.cs
GeneratedModels.cs
GeneratedSerializer.cs

might contain tens of thousands of lines.

If those files are generated deterministically from a schema, reviewing every generated line may provide little value.

A better workflow can focus review attention on:

Source schema
Generation configuration
Generator changes
Handwritten integration code

This reduces noise without pretending that generated changes do not exist.

Common Mistakes When Building Large Diff Viewers

Rendering Everything at Once

This is the most obvious scalability problem.

Loading Every File Before the User Opens It

This increases unnecessary network and processing costs.

Performing Syntax Highlighting for Hidden Content

Only visible or requested content should normally require expensive rendering work.

Creating Large DOM Trees

A massive DOM can create memory and layout problems.

Ignoring Mobile and Low-Memory Devices

A solution that works on a powerful development machine may behave differently on less capable hardware.

Treating Server Performance and Browser Performance as the Same Problem

Both need separate optimization strategies.

Troubleshooting Large Diff Performance

The Page Takes Too Long to Open

Check whether the client is downloading the complete diff before displaying anything.

Incremental loading can reduce initial work.

Scrolling Is Slow

Inspect DOM size and rendering frequency.

Virtualization can help when too many elements are being rendered simultaneously.

Browser Memory Usage Is High

Look for:

  • Huge response objects

  • Large DOM trees

  • Duplicate data

  • Cached content

  • Syntax-highlighted content that is not visible

Opening a Large File Freezes the Browser

Avoid processing the entire file synchronously.

Load and render the visible portion first.

Server Response Is Slow

Measure separately:

Diff generation
Serialization
Network transfer
Client parsing
Client rendering

Do not assume the browser is always responsible for the delay.

Advantages of Scalable Diff Rendering

  1. Better browser performance - Only the necessary content is rendered.

  2. Lower initial loading cost - Users can see useful content sooner.

  3. Better memory usage - The browser does not need to maintain millions of active DOM elements.

  4. Improved navigation - Large datasets can be loaded progressively.

  5. Supports very large repositories - The architecture can scale beyond small pull requests.

Disadvantages and Trade-Offs

  1. Implementation is more complex.

  2. Virtualized interfaces require careful scroll and layout handling.

  3. Lazy loading introduces additional client-server requests.

  4. Caching increases system complexity.

  5. Very large changes can still be difficult for humans to understand.

  6. Rendering optimization does not solve code-review quality problems.

What Developers Can Learn From This

The engineering lessons behind large pull request rendering apply far beyond GitHub.

The same principles are useful when building:

  • Log viewers

  • Database management tools

  • IDEs

  • Monitoring dashboards

  • Large data tables

  • File explorers

  • Code search interfaces

  • Test result viewers

Whenever an application needs to display millions of records, ask:

Do I need to load everything?
Do I need to process everything?
Do I need to render everything?
Does the user need everything immediately?

The answer is usually no.

Best Practices for Large Code Review Systems

  • Load only the information required for the current view.

  • Use virtualization for large lists and code regions.

  • Separate file indexing from diff rendering.

  • Delay syntax highlighting until content is visible.

  • Cache expensive operations where appropriate.

  • Avoid unnecessary generated-file processing.

  • Measure server and browser performance separately.

  • Keep the initial response small.

  • Test with realistic large datasets.

  • Test memory consumption as well as response time.

  • Keep pull requests logically focused when possible.

Conclusion

Rendering a million-line pull request is not primarily a problem of displaying more text. It is a systems problem involving backend processing, data transfer, browser memory, DOM management, syntax highlighting, navigation, and review metadata.

The most important architectural idea is to avoid treating the entire pull request as something that must be loaded and rendered at once.

Incremental loading, lazy processing, caching, and virtualization allow a system to work with extremely large datasets while keeping the amount of active UI relatively small.

There is also an important lesson for developers: a system being capable of handling a million-line pull request does not mean a million-line pull request is easy to review.

Good engineering practices still matter. Smaller logical changes, focused pull requests, clear commit boundaries, and separation of generated code can make the actual review process much easier.

Large-scale rendering technology makes huge pull requests more manageable, but it does not remove the need for thoughtful software design or careful human review.