Skip to content

Commit fefd953

Browse files
perf(web): the hosted browser webview compiles with the React Compiler
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 7111501 commit fefd953

2 files changed

Lines changed: 51 additions & 35 deletions

File tree

‎apps/web/reactCompiledFiles.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,5 @@ export const REACT_COMPILED_FILES = [
9696
"apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx",
9797
"apps/web/src/components/ThreadTerminalDrawer.tsx",
9898
"apps/web/src/components/settings/SettingsPanels.tsx",
99+
"apps/web/src/browser/HostedBrowserWebview.tsx",
99100
] as const;

‎apps/web/src/browser/HostedBrowserWebview.tsx‎

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ import type {
66
ScopedThreadRef,
77
} from "@t3tools/contracts";
88
import { useShallow } from "zustand/react/shallow";
9-
import { useCallback, useEffect, useRef, useState } from "react";
9+
import { useEffect, useEffectEvent, useRef, useState } from "react";
1010

1111
import { previewBridge } from "~/components/preview/previewBridge";
1212
import { usePreviewBridge } from "~/components/preview/usePreviewBridge";
1313
import { useClientSettingsHydrated } from "~/hooks/useSettings";
1414
import { cn, isMacPlatform } from "~/lib/utils";
1515

16-
import { resolveBrowserSurfacePanelRect, useBrowserSurfaceStore } from "./browserSurfaceStore";
16+
import {
17+
type BrowserSurfaceContentPresentation,
18+
type BrowserSurfaceRect,
19+
resolveBrowserSurfacePanelRect,
20+
useBrowserSurfaceStore,
21+
} from "./browserSurfaceStore";
1722
import { useActiveBrowserRecordingTabIds } from "./browserRecording";
1823
import {
1924
browserViewportSettingKey,
@@ -136,15 +141,14 @@ export function HostedBrowserWebview(props: {
136141

137142
const [webviewGeneration, setWebviewGeneration] = useState(0);
138143
const [recoverySrc, setRecoverySrc] = useState(initialSrc);
139-
const latestUrlRef = useRef(initialUrl);
140-
141-
useEffect(() => {
142-
latestUrlRef.current = initialUrl;
143-
}, [initialUrl]);
144+
const remountCrashedWebview = useEffectEvent(() => {
145+
setRecoverySrc(initialUrl ?? initialSrc);
146+
setWebviewGeneration((generation) => generation + 1);
147+
});
144148

145-
const setWebviewRef = useCallback((node: HTMLElement | null) => {
149+
const setWebviewRef = (node: HTMLElement | null) => {
146150
webviewRef.current = node as ElectronWebview | null;
147-
}, []);
151+
};
148152

149153
useEffect(() => {
150154
const webview = webviewRef.current;
@@ -176,10 +180,7 @@ export function HostedBrowserWebview(props: {
176180
crashRecoveryRef.current = recovery.state;
177181
recoveryTimeout = setTimeout(() => {
178182
recoveryTimeout = null;
179-
if (!disposed) {
180-
setRecoverySrc(latestUrlRef.current ?? initialSrc);
181-
setWebviewGeneration((generation) => generation + 1);
182-
}
183+
if (!disposed) remountCrashedWebview();
183184
}, recovery.delayMs);
184185
};
185186
// A click inside the guest only reaches this document as a webview focus
@@ -203,7 +204,13 @@ export function HostedBrowserWebview(props: {
203204
webview.removeEventListener("render-process-gone", recoverGuest);
204205
webview.removeEventListener("focus", dismissHostPopups);
205206
};
206-
}, [clientSettingsHydrated, config, initialSrc, runtimeTabId, webviewGeneration]);
207+
}, [
208+
clientSettingsHydrated,
209+
config,
210+
runtimeTabId,
211+
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A crash recovery remounts the webview under a new key, so the listeners attach to the new element.
212+
webviewGeneration,
213+
]);
207214

208215
const active = presentation.visible && presentation.rect !== null;
209216
const lastRect = presentation.rect;
@@ -214,25 +221,10 @@ export function HostedBrowserWebview(props: {
214221
viewportWidth === null || viewportHeight === null ? null : viewportWidth / viewportHeight;
215222
const lockedAspectRatio =
216223
aspectRatioLocked && viewportAspectRatio !== null ? viewportAspectRatio : null;
217-
const handleAspectRatioChange = useCallback((aspectRatio: number | null) => {
224+
const handleAspectRatioChange = (aspectRatio: number | null) => {
218225
setAspectRatioLocked(aspectRatio !== null);
219-
}, []);
220-
const hiddenContentSize = presentation.content
221-
? {
222-
width: presentation.content.width / presentation.content.scale,
223-
height: presentation.content.height / presentation.content.scale,
224-
}
225-
: null;
226-
const hiddenSize =
227-
viewport._tag !== "fill"
228-
? {
229-
width: viewport.width * normalizedZoomFactor,
230-
height: viewport.height * normalizedZoomFactor,
231-
}
232-
: {
233-
width: hiddenContentSize?.width ?? lastRect?.width ?? 1280,
234-
height: hiddenContentSize?.height ?? lastRect?.height ?? 800,
235-
};
226+
};
227+
const hiddenSize = useHiddenSize(viewport, normalizedZoomFactor, presentation.content, lastRect);
236228
const containerSize = active && lastRect ? lastRect : hiddenSize;
237229
const deviceToolbarVisible = active && viewport._tag !== "fill" && !presentation.fitSourceContent;
238230
const {
@@ -263,7 +255,7 @@ export function HostedBrowserWebview(props: {
263255
? resolveBrowserViewportLayout(lastRect, fittedSourceViewport, normalizedZoomFactor)
264256
: viewportLayout;
265257

266-
const syncContentPresentation = useCallback(() => {
258+
const syncContentPresentation = () => {
267259
const wrapper = wrapperRef.current;
268260
if (!wrapper) return;
269261
useBrowserSurfaceStore.getState().presentContent(runtimeTabId, {
@@ -275,7 +267,7 @@ export function HostedBrowserWebview(props: {
275267
scrollLeft: wrapper.scrollLeft,
276268
scrollTop: wrapper.scrollTop,
277269
});
278-
}, [layout, runtimeTabId]);
270+
};
279271

280272
useEffect(() => {
281273
const frameId = window.requestAnimationFrame(syncContentPresentation);
@@ -286,7 +278,13 @@ export function HostedBrowserWebview(props: {
286278
const wrapper = wrapperRef.current;
287279
if (!wrapper) return;
288280
wrapper.scrollTo({ left: 0, top: 0 });
289-
}, [runtimeTabId, viewport._tag, viewportHeight, viewportWidth]);
281+
}, [
282+
// oxlint-disable-next-line react/exhaustive-effect-dependencies -- A new tab or viewport size starts the canvas scrolled to its origin.
283+
runtimeTabId,
284+
viewport._tag,
285+
viewportHeight,
286+
viewportWidth,
287+
]);
290288

291289
if (!clientSettingsHydrated || !config) return null;
292290

@@ -392,3 +390,20 @@ export function HostedBrowserWebview(props: {
392390
</div>
393391
);
394392
}
393+
394+
// The size feeds useBrowserViewportResize, whose layout keys the content sync effect.
395+
function useHiddenSize(
396+
viewport: PreviewViewportSetting,
397+
zoomFactor: number,
398+
content: BrowserSurfaceContentPresentation | null,
399+
lastRect: BrowserSurfaceRect | null,
400+
) {
401+
"use memo";
402+
if (viewport._tag !== "fill") {
403+
return { width: viewport.width * zoomFactor, height: viewport.height * zoomFactor };
404+
}
405+
return {
406+
width: content ? content.width / content.scale : (lastRect?.width ?? 1280),
407+
height: content ? content.height / content.scale : (lastRect?.height ?? 800),
408+
};
409+
}

0 commit comments

Comments
 (0)