From 7fc2967793023bc35dc2e81b50c2669c78229ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 18 May 2026 15:46:18 -0400 Subject: [PATCH] fix(servo): skip WebView::load when Servo already at requested URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Servo's \`WebView::set_history\` fires \`notify_url_changed\` on **every** history mutation — full navigations, redirects, in-page link clicks, and JS-driven \`history.pushState\` / \`history.replaceState\`. Our pipeline fans that delegate signal back through \`WebSurfaceUrlChange\` into \`tab.url\`. The next \`ensure_surface\` observes the new tab URL, hashes a fresh ensure key, and calls \`ServoLiveClient::ensure\` → \`apply_navigation\`. Until this commit \`apply_navigation\` compared the request against the local \`session.requested_url\` cache and, on mismatch, sent Servo a \`webview.load(url)\` — even when Servo was the one who *just* told us about that URL. \`WebView::load\` is a hard navigation: it tells the constellation to abort the current document, clear the surface, and refetch. google.com's homepage \`replaceState\`s a fresh \`?zx=\` roughly once a second to bust caches; with this round-trip we were turning each of those into a full load-clear-refetch and producing one visible white flash per second. Use \`host.snapshot(webview_id).url()\` (which Servo keeps in lock-step with \`set_history\`) as the source of truth. If Servo's WebView is already at the requested URL, just sync our \`session.requested_url\` bookkeeping and return — no \`load\` message, no surface clear, no flash. Genuine embedder-initiated navigations (chrome URL bar typed, in-app link click) still take the \`should_navigate\` path because Servo's URL hasn't caught up to the requested target yet. --- crates/ely_app/src/services/servo_live.rs | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/ely_app/src/services/servo_live.rs b/crates/ely_app/src/services/servo_live.rs index 0ac8c0b..5133318 100644 --- a/crates/ely_app/src/services/servo_live.rs +++ b/crates/ely_app/src/services/servo_live.rs @@ -234,6 +234,33 @@ impl ServoLiveClient { tab_id: TabId, requested_url: UrlText, ) -> Result<(), ServoLiveError> { + // Servo's `set_history` fires `notify_url_changed` on *every* + // history mutation — full navigations, redirects, in-page + // links, and JS-driven `history.pushState` / `replaceState`. + // Our embedder fans that back through `WebSurfaceUrlChange` + // into `tab.url`, which makes the next `ensure_surface` see a + // "different" URL and call back into this method. Without a + // guard we then send Servo `WebView::load(new_url)` for a URL + // Servo just informed us it is *already* at — and `load` is a + // hard navigation that aborts the live document, clears the + // surface, and refetches. + // + // The google.com homepage `replaceState`s a `?zx=` + // every second; under the unguarded path that turned into a + // load-clear-refetch cycle per second, i.e. the continuous + // white flash. Servo's own `webview.url()` (driven by + // `set_history`) is the source of truth — if it already + // matches the requested URL, this URL change came *from* + // Servo and only needs an embedder-side bookkeeping sync. + let servo_current_url = + self.host.snapshot(webview_id)?.url().map(str::to_string); + if servo_current_url.as_deref() == Some(requested_url.as_str()) { + if let Some(session) = self.sessions.get_mut(&request.tab_id) { + session.requested_url = Some(requested_url.as_str().to_string()); + } + return Ok(()); + } + let should_navigate = self .sessions .get(&request.tab_id)