Viewport navigation predictor under DevTools mobile emulation

#420724833 Filed 2025-05-28 Reporter: domenic@chromium.org Status: New / P3 Feature

Issue summary

The deterministic viewport-based predictor for moderate eagerness speculation rules (kNavigationPredictorNewViewportFeatures + kPreloadingModerateViewportHeuristics) is #ifdef BUILDFLAG(IS_ANDROID)-gated and thus disabled on desktop. As a result, web developers using DevTools mobile emulation cannot observe or test the predictor that ships to Android users.

This CL flips the IS_ANDROID-keyed defaults so the predictor and its tunables behave the same on every platform speculation rules ship to. Mobile emulation in DevTools therefore automatically exercises the heuristic, and so does normal desktop browsing. WebView keeps its existing opt-out via android_webview/browser/aw_field_trials.cc, and Finch can still disable per-platform if a field experiment wants to.

A more conservative variant (runtime-gate the heuristic on Settings::ViewportEnabled(), which only flips under DevTools mobile emulation on desktop) was prototyped but had too many follow-on browser-side gates to be a tidy CL. The behavior change for desktop end users is small (capped at two concurrent moderate-eagerness prefetches, only on visible anchors that are larger than their nearest runner-up by >25%), so the simpler default-on path is the recommended fix.

The companion concern in gilbertoc@google.com's comment -- that even when the prefetch fires under emulation, the prefetched response may be the desktop variant of the page because PrefetchService's outgoing request doesn't carry the DevTools-emulated UA / Client Hints / viewport headers -- is a separate plumbing problem and is out of scope of this CL. See "Known follow-ups" at the bottom.

Reproducer pages

→ heuristic.html (source) → target.html (destination) dummy-1.html dummy-2.html dummy-3.html

What's on each page

heuristic.html -- source page with one visually-dominant "hero" anchor and three smaller runner-ups. A <script type="speculationrules"> document rule registers every a.anchor-card as a moderate-eagerness prefetch candidate. The moderate viewport heuristic, when it runs, picks the largest in-viewport anchor and enacts that pending candidate.

target.html -- destination of the hero. Reads PerformanceNavigationTiming on load and prints a verdict (prefetch hit / possibly prefetched / cold fetch) based on deliveryType, transferSize, and response time.

dummy-1/2/3.html -- runner-up destinations. Should not be picked by the heuristic when the hero is the largest in-viewport anchor.

Build matrix

Build Heuristic fires?
Stock canary, desktop no — this is the bug
Stock canary, desktop, with all three feature flags forced on via --enable-features (see "approximation" command below) yes (prove the chain works without rebuilding)
Patched canary (branch i-420724833, commit dbf6d0c442b96), desktop yes — this is the fix
Any build, real Android yes (already works today)

Step-by-step manual verification

A. Pre-patch (verify the bug)

  1. Launch a stock Chrome Canary on desktop:
    google-chrome-canary --user-data-dir=/tmp/i-420724833-prepatch
  2. Open DevTools (F12), dock to the right, switch to Application -> Speculative loads -> Preloads.
  3. Optional: toggle the Device toolbar to iPhone / Pixel. The heuristic stays off either way; this is the user-facing bug for developers who try DevTools mobile emulation.
  4. Open https://static.januschka.com/i-420724833/heuristic.html. The Rules tab should show one ruleset with 4 moderate-eagerness prefetch candidates registered.
  5. Move the mouse to a corner so hover doesn't pollute the trigger column. Scroll with the keyboard (Space, Page Down) so the gradient HERO is the dominant in-viewport anchor. Wait ~600ms.
  6. Click each row in Preloads and check the Trigger / Predictor column in the right-hand details panel. None of them shows trigger Moderate viewport heuristic. Any rows that flipped to Ready are coming from the hover heuristic on links you happened to mouse over -- not the heuristic we care about.
  7. Click the hero anchor. target.html's verdict box reports NOT PREFETCHED with transferSize > 0 and a normal RTT.

B. Approximation on a stock binary (no rebuild)

To prove the rest of the predictor pipeline works without rebuilding, you can launch any unpatched canary with all three Android-only-on defaults forced on:

google-chrome-canary \
  --user-data-dir=/tmp/i-420724833-flagged \
  --enable-features=NavigationPredictorNewViewportFeatures,PreloadingModerateViewportHeuristics:enact_candidates/true,NavigationPredictor:random_anchor_sampling_period/1

With these flags applied, the same scroll interaction in step A should fire the moderate viewport heuristic on the hero. If this works on your binary but the patched build doesn't, the patch isn't actually applied to the binary you launched.

C. Patched build (verify the fix)

  1. Build Chromium from this branch:
    cd ~/chromium/src
    git fetch origin
    git checkout -b i-420724833 origin/main
    git cherry-pick dbf6d0c442b96   # 3-file CL
    autoninja -C out/Default chrome
  2. Launch the patched binary with a fresh profile and no --enable-features:
    out/Default/chrome --user-data-dir=/tmp/i-420724833-postpatch
  3. DevTools -> Application -> Speculative loads -> Preloads.
  4. Optional: toggle the Device toolbar to a mobile profile to demonstrate parity with Android. The heuristic fires either way; emulation is no longer required.
  5. Open https://static.januschka.com/i-420724833/heuristic.html.
  6. Move the mouse to a corner so hover doesn't pollute the trigger column. Scroll with the keyboard so the hero is the dominant in-viewport anchor. Wait ~600ms.
  7. Within ~500ms (kPreloadingModerateViewportHeuristics.delay) the hero's row flips to Ready:
    • URL: .../i-420724833/target.html?from=heuristic
    • Action: prefetch
    • Eagerness: moderate
    • Trigger / Predictor: Moderate viewport heuristic
    The runner-ups stay Not triggered unless you hover them.
  8. Click the hero. target.html's verdict box reports LIKELY SERVED FROM PREFETCH CACHE, with transferSize = 0 and a non-zero decodedBodySize.

What "passing" looks like

Check Pre-patch Post-patch
Speculative loads panel shows trigger Moderate viewport heuristic no yes (within ~500ms)
target.html verdict cold fetch prefetch cache hit
Behavior under DevTools mobile emulation heuristic still off heuristic on, same as without emulation

Patch summary

Branch i-420724833, commit dbf6d0c442b96, 3 files / +19 / -19.

Tests run

out/Default/blink_unittests \
  --gtest_filter='AnchorElement*:NavigationPredictor*:AnchorElementInteraction*'
  -> 61/61 passed

xvfb-run -a out/Default/unit_tests \
  --gtest_filter='NavigationPredictor*'
  -> 31/31 passed

xvfb-run -a out/Default/content_unittests \
  --gtest_filter='AnchorElementInteractionHostImplTest.*:PreloadingDeciderTest.*'
  -> 21/21 passed

Known follow-ups (out of scope)

gilbertoc@google.com noted that even when the moderate viewport heuristic fires under emulation, the prefetched response may be the desktop variant of the page because PrefetchService's outgoing request doesn't carry the DevTools-emulated User-Agent, Client Hints, or viewport headers.

Plumbing the per-frame UA / Client Hints overrides through to the prefetch fetcher (so the prefetched HTML matches what the eventually-navigated request would receive) is a separate change in content/browser/preloading/prefetch/. It is not addressed by this CL; tracking under the same bug is the expected follow-up.