fix(servo): skip WebView::load when Servo already at requested URL

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=<timestamp>\`
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.
This commit is contained in:
2026-05-18 15:46:18 -04:00
parent 0a97c4af13
commit 7fc2967793
+27
View File
@@ -234,6 +234,33 @@ impl ServoLiveClient {
tab_id: TabId, tab_id: TabId,
requested_url: UrlText, requested_url: UrlText,
) -> Result<(), ServoLiveError> { ) -> 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=<timestamp>`
// 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 let should_navigate = self
.sessions .sessions
.get(&request.tab_id) .get(&request.tab_id)