All posts
Published March 12, 2025 in reports

Anthropic Sonnet 3.7 Broke our Diff Viewer

Anthropic Sonnet 3.7 Broke our Diff Viewer
Author: Tiger at Lovable

Introduction

When we upgraded to Claude 3.7 Sonnet, we expected and welcomed the increased output capabilities. What we didn't anticipate was discovering a performance bottleneck in our Diff Viewer. As the number of modified files increased, the browser would freeze.

In this post, we'll dive into the problems, alternatives considered, and the solution we implemented.

Here's a look at our Diff Viewer:

code diff viewer on Lovable

TL;DR

  • Each CodeMirror instance takes 20-50ms to load and blocks the browser while loading.
  • When we load many editors at once (more than 15), the browser freezes because all editors load one after another.
  • Our solution: break the work into smaller chunks (time slicing) to keep the browser responsive.

The Problem

Claude 3.7 Sonnet itself wasn't the issue - it was the increase in output compared to 3.5. With more output, our app now can do more changes to more files at once.

We use CodeMirror to render code diffs. Loading a few CodeMirror instances isn't problematic, but when displaying 15+ files at once, you would start to notice the browser freeze. This can take anywhere from 500ms to 1500ms depending on your machine.

Why? Each CodeMirror instance takes 20-50ms to initialize and blocks the browser while doing so. We were loading all instances at once, creating one long blocking task. During this time, users couldn't interact with any part of the page.

When profiling the performance, we observed a single long task on the main thread of about 500ms on a high-performance machine for ~20 files. On slower devices or with network throttling, this would be worse.

Profiling

Understanding Forced Reflows

In the performance profile image above, you'll notice purple bars with red markers. These indicate forced reflows, which contribute to our performance problem.

What's a reflow? It's when the browser recalculates the layout of elements on your page. Normally, browsers batch these calculations for efficiency. But a "forced reflow" happens when JavaScript interrupts this process by:

  1. Making a change to the DOM (like adding or modifying an element)
  2. Then immediately requesting layout information (like asking for an element's size)

This pattern forces the browser to calculate layout immediately, making the process more expensive.

CodeMirror's initialization process triggers these forced reflows - a known issue documented in several GitHub issues. These unnecessary reflows make each editor instance even more expensive to initialize than it should be, compounding our performance problem.

Finding the Right Approach

When tackling this performance issue, we explored several potential solutions:

  1. Collapse all files by default

    • How it works: Initialize CodeMirror only when user expands a file
    • Drawback: Forces users to manually click each file they want to see
  2. Show first 3 files, collapse the rest

    • How it works: Immediately load the first 3 files, initialize others only when clicked
    • Drawback: Still requires manual expansion to see most changes
  3. Expand files as they enter viewport

    • How it works: Use intersection observer to load files as user scrolls to them
    • Drawback: More complex implementation with potential flickering during fast scrolling since initialization of a CodeMirror instance itself takes time

While each approach was good, our core requirements remained:

  • Maintain a seamless user experience with minimal manual intervention
  • Eliminate the browser-freezing long task

After evaluating these options, we landed on a different solution: time slicing. This technique initializes CodeMirror instances in small batches, yielding control back to the browser between batches.

The result? One long blocking task becomes several shorter ones, keeping the interface responsive while still loading all content.

Solution: Time Slicing with React

Instead of loading all files at once, we:

  1. Render only a small batch of files immediately (2 at a time)
  2. Show loading placeholders for the remaining files
  3. Gradually replace placeholders with actual content
  4. Add small delays (50ms) between each batch to let the browser stay responsive

This approach turns one long blocking task into several shorter ones with gaps in between. During these gaps, the browser can handle user interactions and maintain responsiveness.

Here's how this pattern can be implemented in React:

function BatchRenderer({ items }) {
  // Start by rendering just the first 2 items
  const [renderedCount, setRenderedCount] = useState(Math.min(2, items.length));
  const timeoutRef = useRef(null);

  // Clean up timeout when component unmounts
  useEffect(() => {
    return () => {
      if (timeoutRef.current) clearTimeout(timeoutRef.current);
    };
  }, []);

  // Schedule rendering of next batch
  useEffect(() => {
    if (renderedCount < items.length) {
      timeoutRef.current = setTimeout(() => {
        setRenderedCount((prevCount) => Math.min(prevCount + 2, items.length));
      }, 50); // 50ms delay between batches
    }
  }, [renderedCount, items.length]);

  return (
    <div>
      {items.map((item, index) => (
        <div key={item.id}>
          {index < renderedCount ? (
            // Render actual content for batched items
            <ExpensiveComponent data={item} />
          ) : (
            // Show placeholder for items not yet rendered
            <PlaceholderComponent />
          )}
        </div>
      ))}
    </div>
  );
}

With this implementation, even when displaying dozens of files, the interface remains responsive throughout the loading process.

As you can see on the image below, instead of one long blocking task, we have several smaller ones.

Profiling solution after fix on Sonnet 3.7

Improved User Experience

The time slicing approach not only solved the technical performance issue but also created a much smoother visual experience.

Before this change, users would see all diffs collapsed while initialization (of all instances) happened behind the scenes. Then suddenly, all diffs would expand at once after a noticeable delay - creating a jarring effect.

With our batched approach, the first two diffs appear almost immediately while others load progressively. This creates a more natural, responsive feeling as content flows in smoothly rather than appearing all at once.

Wrapping Up

Time slicing solved our browser freezing issue. We turned one long task into several short ones, keeping the interface responsive while still loading all files.

This approach works for any expensive UI operations, not just CodeMirror. If you encounter a similar issue, try time slicing to break up long tasks and keep your app responsive.

Small changes can make a big difference for performance.

Idea to app in seconds

Build apps by chatting with an AI.

Start for free