[eric] canvas: webview attaches go one per frame, so opening a loaded dashboard stops locking the app for 4.7s

This commit is contained in:
ciregenz
2026-08-07 13:39:03 -07:00
parent c0c190987e
commit 5eed1385be
2 changed files with 60 additions and 1 deletions
@@ -1,4 +1,5 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { requestWebviewAttachSlot } from './webviewAttachQueue';
import { createPortal } from 'react-dom';
import { subscribeLiveDrag } from '../hooks/interaction/liveDragChannel';
import Box from '@mui/material/Box';
@@ -416,6 +417,15 @@ const BrowserCard: React.FC<Props> = ({
}, [activeUrl, activeTabId]);
const webviewMap = useRef<Map<string, WebviewElement>>(new Map());
// Electron attaches a guest with a SYNCHRONOUS renderer IPC, so a dashboard that mounts N cards
// puts N blocking round-trips in one frame (measured: 4755ms over 40 long tasks at 18 cards).
// Waiting for a slot spreads them one per frame; nothing unmounts, so sessions are untouched.
const [attachSlotReady, setAttachSlotReady] = useState(false);
useEffect(() => {
if (attachSlotReady) return undefined;
return requestWebviewAttachSlot(() => setAttachSlotReady(true));
}, [attachSlotReady]);
const initializedTabs = useRef(new Set<string>());
const tabBarRef = useRef<HTMLDivElement>(null);
// Some pages (Zillow's map) rewrite their own URL many times a second, across did-navigate-in-page AND did-stop-loading; throttle the persisted URL mirror so each tick can't fan out to a full dashboard save + webview suspend re-eval. Leading edge keeps a real navigation's URL immediate.
@@ -1636,7 +1646,7 @@ const BrowserCard: React.FC<Props> = ({
)
) : (
<>
{tabs.map((tab) => (
{(attachSlotReady ? tabs : []).map((tab) => (
<webview
key={tab.id}
ref={(el: any) => {
@@ -0,0 +1,49 @@
/**
* Serialises <webview> attachment to one per frame.
*
* Electron attaches a guest view with a SYNCHRONOUS renderer IPC (GUEST_VIEW_MANAGER_CALL), so N
* cards mounting together put N blocking round-trips in one frame. Measured on a real dashboard:
* opening one with 18 cards / 8 webviews blocked the main thread for 4755ms across 40 long tasks,
* while an idle canvas blocked for 0ms (ENG-193). Nothing here makes the attach cheaper; it just
* stops them landing on the same frame, so the UI keeps painting between them.
*/
type Slot = () => void;
let pending: Slot[] = [];
let pumping = false;
function pump(): void {
const next = pending.shift();
if (!next) {
pumping = false;
return;
}
try {
next();
} catch {
/* a card that blew up on attach must not stall every card behind it */
}
requestAnimationFrame(() => pump());
}
/**
* Ask for the next attach slot. `onReady` fires on this frame if the queue is empty, otherwise one
* frame per card ahead of it. Returns a cancel function for unmount before the slot arrives.
*/
export function requestWebviewAttachSlot(onReady: Slot): () => void {
pending.push(onReady);
if (!pumping) {
pumping = true;
// Start on the next frame so a burst of cards mounting in one commit all queue up first.
requestAnimationFrame(() => pump());
}
return () => {
pending = pending.filter((s) => s !== onReady);
};
}
/** Cards waiting behind the queue right now; exposed so a test can prove the burst is serialised. */
export function pendingAttachCount(): number {
return pending.length;
}