diff --git a/Sources/BrowserWindowPortal.swift b/Sources/BrowserWindowPortal.swift index 646e8aba..272c0cc7 100644 --- a/Sources/BrowserWindowPortal.swift +++ b/Sources/BrowserWindowPortal.swift @@ -2904,7 +2904,11 @@ final class WindowBrowserPortal: NSObject { synchronizeWebView(withId: primaryWebViewId, source: "anchorPrimary") } - synchronizeAllWebViews(excluding: primaryWebViewId, source: "anchorSecondary") + // During rapid geometry changes (e.g. divider drag), syncing every web view + // on every frame is expensive and causes stuttering. Each panel's + // HostContainerView fires its own geometry callback, so secondary web views + // will sync themselves. Defer the all-sync to coalesce with the next + // run-loop turn instead. scheduleDeferredFullSynchronizeAll() } diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 2e401164..9f8dd05a 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -4313,6 +4313,7 @@ struct WebViewRepresentable: NSViewRepresentable { var onGeometryChanged: (() -> Void)? private(set) var geometryRevision: UInt64 = 0 private var lastReportedGeometryState: GeometryState? + private var hasPendingGeometryNotification = false private weak var hostedWebView: WKWebView? private var hostedWebViewConstraints: [NSLayoutConstraint] = [] private weak var localInlineSlotView: WindowBrowserSlotView? @@ -4647,7 +4648,30 @@ struct WebViewRepresentable: NSViewRepresentable { ) } + /// Record that geometry changed without firing the callback immediately. + /// `setFrameOrigin`/`setFrameSize` can fire multiple times before `layout()`; + /// deferring avoids redundant portal-sync cascades during divider drag. + /// A dispatch fallback ensures the callback fires even if `layout()` is not called. + /// Note: `lastReportedGeometryState` and `geometryRevision` are only updated + /// when the callback actually fires, so `updateNSView` sees a revision that + /// is strictly tied to emitted callbacks (no premature increments). + private func markGeometryDirtyIfNeeded() { + let state = currentGeometryState() + guard state != lastReportedGeometryState else { return } + guard !hasPendingGeometryNotification else { return } + hasPendingGeometryNotification = true + DispatchQueue.main.async { [weak self] in + self?.notifyGeometryChangedIfNeeded() + } + } + + /// Check for geometry changes and fire the callback. Also flushes any pending + /// dirty state from `markGeometryDirtyIfNeeded` so `layout()` supersedes the + /// async fallback. Only updates `lastReportedGeometryState` / `geometryRevision` + /// when the callback is emitted, keeping the revision in sync with actual + /// notifications. private func notifyGeometryChangedIfNeeded() { + hasPendingGeometryNotification = false let state = currentGeometryState() guard state != lastReportedGeometryState else { return } lastReportedGeometryState = state @@ -5070,7 +5094,8 @@ struct WebViewRepresentable: NSViewRepresentable { override func setFrameOrigin(_ newOrigin: NSPoint) { super.setFrameOrigin(newOrigin) window?.invalidateCursorRects(for: self) - notifyGeometryChangedIfNeeded() + // Mark dirty; the callback fires from layout() with the settled geometry. + markGeometryDirtyIfNeeded() #if DEBUG debugLogHostedInspectorLayoutIfNeeded(reason: "setFrameOrigin") #endif @@ -5079,7 +5104,8 @@ struct WebViewRepresentable: NSViewRepresentable { override func setFrameSize(_ newSize: NSSize) { super.setFrameSize(newSize) window?.invalidateCursorRects(for: self) - notifyGeometryChangedIfNeeded() + // Mark dirty; the callback fires from layout() with the settled geometry. + markGeometryDirtyIfNeeded() #if DEBUG debugLogHostedInspectorLayoutIfNeeded(reason: "setFrameSize") #endif