Fix panel resize stuttering when tiled with browser panels (#1969)

* Fix panel resize stuttering when tiled with browser panels (#1968)

During divider drag, the portal sync system was doing O(N²) work per
frame: each geometry callback synced ALL web views, and multiple
callbacks fired per layout pass (setFrameSize + setFrameOrigin + layout).

Two changes:

1. synchronizeWebViewForAnchor now only syncs the primary web view and
   defers the all-sync. Each panel fires its own geometry callback, so
   secondary syncs are redundant on the hot path.

2. HostContainerView.setFrameOrigin/setFrameSize use markGeometryDirtyIfNeeded
   which defers the callback to layout(), coalescing 2-3 notifications
   per frame into one. An async fallback ensures origin-only changes
   (without a subsequent layout) are still delivered.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix premature geometryRevision increment in markGeometryDirtyIfNeeded

Address reviewer feedback (Greptile, CodeRabbit): geometryRevision and
lastReportedGeometryState are now only updated when the callback
actually fires, not eagerly.  This prevents updateNSView from seeing a
premature revision delta and triggering a redundant synchronizeForAnchor
before the coalesced notification arrives.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Elvis Tran 2026-03-25 15:54:17 +10:30 committed by GitHub
parent a395e8c343
commit b919541044
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 33 additions and 3 deletions

View file

@ -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()
}

View file

@ -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