Skip to content

Commit c298a40

Browse files
perf(web): diff file boundaries observe visibility once instead of on every render (#25134)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3ddfcfc commit c298a40

5 files changed

Lines changed: 94 additions & 20 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, it, vi } from "vite-plus/test";
2+
3+
import { render } from "../../../test/render";
4+
import { DiffFileLoadingBoundary } from "./DiffFileLoadingBoundary";
5+
6+
// Like the real observer, every observe() reports the target's current state; this boundary
7+
// is always in range.
8+
class InRangeIntersectionObserver {
9+
static observeCount = 0;
10+
constructor(readonly callback: IntersectionObserverCallback) {}
11+
observe(target: Element) {
12+
InRangeIntersectionObserver.observeCount += 1;
13+
this.callback(
14+
[{ target, isIntersecting: true } as IntersectionObserverEntry],
15+
this as unknown as IntersectionObserver,
16+
);
17+
}
18+
unobserve() {}
19+
disconnect() {}
20+
}
21+
22+
function stubIntersectionObserver() {
23+
InRangeIntersectionObserver.observeCount = 0;
24+
vi.stubGlobal("IntersectionObserver", InRangeIntersectionObserver);
25+
return { [Symbol.dispose]: () => vi.unstubAllGlobals() };
26+
}
27+
28+
describe("DiffFileLoadingBoundary", () => {
29+
it("observes once and loads once while its parent re-renders with a new load", async () => {
30+
using _observer = stubIntersectionObserver();
31+
const load = vi.fn();
32+
// DiffPanel hands over a new loadNextFiles whenever any file patch changes.
33+
await using screen = await render(<DiffFileLoadingBoundary load={() => load()} count={8} />, {
34+
router: false, // the boundary does not touch routing
35+
});
36+
await screen.rerender(<DiffFileLoadingBoundary load={() => load()} count={8} />);
37+
await screen.rerender(<DiffFileLoadingBoundary load={() => load()} count={8} />);
38+
await screen.rerender(<DiffFileLoadingBoundary load={() => load()} count={8} />);
39+
40+
await expect.element(screen.getByRole("status", { name: "Loading diff…" })).toBeInTheDocument();
41+
expect(InRangeIntersectionObserver.observeCount).toBe(1);
42+
expect(load).toHaveBeenCalledTimes(1);
43+
44+
// A settled batch leaves the boundary in range, so it asks for the next one.
45+
await screen.rerender(<DiffFileLoadingBoundary load={() => load()} count={4} />);
46+
expect(InRangeIntersectionObserver.observeCount).toBe(1);
47+
expect(load).toHaveBeenCalledTimes(2);
48+
});
49+
});

‎apps/web/src/components/diffs/DiffFileLoadingBoundary.tsx‎

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
1+
import { useEffect, useState } from "react";
2+
3+
import { useStableCallback } from "~/hooks/useStableCallback";
14
import { observeIntersection } from "~/lib/observeIntersection";
25
import { DiffFileHeaderSkeleton } from "../DiffPanelShell";
36

47
export function DiffFileLoadingBoundary({ load, count }: { load: () => void; count: number }) {
8+
const [isNear, setIsNear] = useState(false);
9+
const loadLatest = useStableCallback(load);
10+
// A fresh observe redelivers the current intersection, so this ref must keep its identity
11+
// across renders: it captures nothing but the state setter.
12+
const observe = (node: HTMLDivElement | null) => {
13+
if (!node) return;
14+
return observeIntersection(node, (entry) => setIsNear(entry.isIntersecting), {
15+
rootMargin: "600px",
16+
});
17+
};
18+
// A settled batch shrinks `count` without moving the boundary out of range, and no new
19+
// intersection entry arrives for that, so ask for the next batch whenever it changes.
20+
useEffect(() => {
21+
if (isNear && count > 0) loadLatest();
22+
}, [isNear, count, loadLatest]);
23+
524
return (
6-
<div
7-
ref={(node) => {
8-
if (!node) return;
9-
return observeIntersection(
10-
node,
11-
(entry) => {
12-
if (entry.isIntersecting) load();
13-
},
14-
{ rootMargin: "600px" },
15-
);
16-
}}
17-
role="status"
18-
aria-label="Loading diff…"
19-
>
25+
<div ref={observe} role="status" aria-label="Loading diff…">
2026
{Array.from({ length: Math.min(count, 4) }, (_, index) => (
2127
<div key={index} aria-hidden className="border-b border-border/40">
2228
<DiffFileHeaderSkeleton titleWidth="medium" />

‎apps/web/src/components/diffs/useReviewFilePatches.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,11 @@ export function useReviewFilePatches({
9292
const added = indices.filter((index) => !previous.includes(index));
9393
return added.length === 0 ? current : { scope, indices: [...previous, ...added] };
9494
});
95-
const loadNextFiles = () =>
95+
// The first unsettled file already requested means a batch is in flight; wait for it.
96+
const loadNextFiles = () => {
97+
if (queries.some(({ index }) => index === settledFileCount)) return;
9698
requestFiles(Array.from({ length: 4 }, (_, index) => settledFileCount + index));
99+
};
97100
const requestFile = (index: number) => requestFiles([index]);
98101
const renderableFiles = source
99102
? files.map((file, index): FileDiffMetadata => {

‎apps/web/src/components/permissions/usePermissionStatus.browser.test.tsx‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import { usePermissionStatus } from "./usePermissionStatus";
66
function PermissionProbe({
77
check,
88
label,
9+
enabled = true,
910
}: {
1011
readonly check: () => Promise<{ screen: boolean }>;
1112
readonly label: string;
13+
readonly enabled?: boolean;
1214
}) {
13-
const { status } = usePermissionStatus(async () => check(), { screen: false });
15+
const { status } = usePermissionStatus(async () => check(), { screen: false }, enabled);
1416
return (
1517
<p>
1618
{label}: {status.screen ? "granted" : "missing"}
@@ -32,4 +34,17 @@ describe("usePermissionStatus", () => {
3234
await expect.element(screen.getByText("third: missing")).toBeInTheDocument();
3335
expect(check).toHaveBeenCalledTimes(1);
3436
});
37+
38+
it("checks with the latest check after the caller passes a new one", async () => {
39+
const first = vi.fn(() => new Promise<{ screen: boolean }>(() => {}));
40+
const second = vi.fn(async () => ({ screen: true }));
41+
await using screen = await render(<PermissionProbe check={first} label="probe" />, {
42+
router: false, // the hook does not touch routing
43+
});
44+
await screen.rerender(<PermissionProbe check={second} label="probe" enabled={false} />);
45+
await screen.rerender(<PermissionProbe check={second} label="probe" />);
46+
47+
await expect.element(screen.getByText("probe: granted")).toBeInTheDocument();
48+
expect(first).toHaveBeenCalledTimes(1);
49+
});
3550
});

‎apps/web/src/components/permissions/usePermissionStatus.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useState, useSyncExternalStore } from "react";
22

3+
import { useStableCallback } from "~/hooks/useStableCallback";
34
import { getDocumentActivity, subscribeDocumentActivity } from "~/lib/documentActivity";
45

56
const POLL_INTERVAL_MS = 1500;
@@ -61,16 +62,16 @@ export function createPermissionPoller<Id extends string>(
6162
const noopSubscribe = () => () => {};
6263

6364
/**
64-
* `check` and `initialStatus` are read once, when the component mounts, so the
65-
* subscription only restarts when `enabled` flips. Key the component if what it
66-
* checks can change.
65+
* Each check calls the latest `check`, since callers close over props. `initialStatus` is
66+
* read once, when the component mounts, and the subscription only restarts when `enabled` flips.
6767
*/
6868
export function usePermissionStatus<Id extends string>(
6969
check: () => Promise<Record<Id, boolean>>,
7070
initialStatus: Record<Id, boolean>,
7171
enabled = true,
7272
) {
73-
const [poller] = useState(() => createPermissionPoller(check, initialStatus));
73+
const checkLatest = useStableCallback(check);
74+
const [poller] = useState(() => createPermissionPoller(checkLatest, initialStatus));
7475
const { status, error } = useSyncExternalStore(
7576
enabled ? poller.subscribe : noopSubscribe,
7677
poller.getSnapshot,

0 commit comments

Comments
 (0)