← Speculation Rules - moderate viewport heuristics controls
v152 · origin trial · speculation rules grammar
moderate_viewport_heuristics
The ruleset-level key that carries the heuristic overrides. An optional JSON object at the top level of a speculation-rules rule set, with up to three numeric fields — distance_from_pointer_down, largest_anchor_threshold, and delay — that retune Chrome’s Android “moderate”-eagerness viewport heuristic for the document. Experimental, origin-trial-gated, and not planned to ship as is.
This key only has an effect in documents opted into the SpeculationRulesModerateViewportHeuristicsControl origin trial (Chrome 152–157, Android only; registration and local-flag steps on the overview). There is no specification; every contract on this page is anchored to the explainer and the linked Chromium source files, and the grammar may change or disappear without a deprecation path.
syntax
Formal position: a top-level member of the rule-set object, a sibling of prefetch, prerender, and tag — not a member of any individual rule:
rule-set = {
"moderate_viewport_heuristics": {
"distance_from_pointer_down": [ low, high ], // optional; two numbers
"largest_anchor_threshold": number, // optional
"delay": milliseconds // optional; number
}, // ^ the whole object is optional
"prefetch": [ … ], // and/or "prerender"
}
The key is valid in inline <script type="speculationrules"> blocks and in external rule sets fetched via Speculation-Rules headers alike — parsing happens in the shared rule-set parser (speculation_rule_set.cc), which makes no inline/external distinction for this key.
distance_from_pointer_down
| Type | Two-element array of numbers [low, high] |
|---|---|
| Meaning | The inclusive band a candidate link’s vertical distance from the user’s last pointerdown must fall inside, expressed as a fraction of screen height. Negative values are above the tap point, positive below. Links outside the band are discarded before the dominance test runs |
| Default | [-0.3, 0.0] — the P12.5/P87.5 bounds of the middle 75% of distance_from_ptr_down_ratio values observed for clicked anchors (per the implementation comments); the compiled values are Finch feature parameters and can be adjusted server-side |
| Clamping | Each end is clamped to [-1, 1]; after clamping, high is raised to at least low (high = max(low, high)), so an inverted band degenerates to a single value rather than an empty interval |
| Malformed input | Ignored unless the value is exactly a two-element array whose entries both convert to numbers; a malformed value leaves the default in place and never fails the rule set |
| Effect of widening | A wider band (e.g. [-0.5, 0.2]) lets more links pass the distance filter — more speculation, more candidate ambiguity for the dominance test to resolve. A band of [0, 0] restricts candidates to links level with the last tap |
largest_anchor_threshold
| Type | Number (a ratio, dimensionless) |
|---|---|
| Meaning | How much bigger the largest in-viewport link must be than the second largest before it is picked: the heuristic requires (size(largest) − size(second)) / size(second) > threshold, with size measured as visible area. If the two biggest links are too close in size, no candidate is chosen — the browser does not guess between near-equal targets |
| Default | 0.25 — the implementation comment records this default “was selected arbitrarily and hasn’t been tuned”, which is precisely the kind of value this experiment exists to improve |
| Clamping | Negative values are clamped up to 0 (max(value, 0)); there is no upper clamp |
| Malformed input | Ignored unless the value converts to a number; the default then applies and the rule set is unaffected |
| Effect of changing | 0 always accepts the largest link even when a near-tie exists (more speculation, more wrong guesses); large values make the picker increasingly conservative, suppressing speculation on dense link grids |
delay
| Type | Number, milliseconds |
|---|---|
| Meaning | The dwell time the browser waits after picking a candidate before it triggers the speculative load. If the user scrolls again during the delay, the candidate is dropped — this is the filter that keeps fling-scrolling from wasting fetches on links that merely whip through the viewport |
| Default | 500 ms — like the threshold, the implementation comment records this default “was selected arbitrarily and hasn’t been tuned” |
| Clamping | 10–5000 ms. The implementation enforces a nonzero 10 ms floor “per privacy review” so authors cannot configure the heuristic to fire essentially immediately. The explainer’s field table documents a 0–5000 range; the implementation floor postdates or supersedes it and is the authoritative contract (2026-07-29) |
| Malformed input | Ignored unless the value converts to a number; the default then applies |
| Effect of changing | Lower values speculate sooner after a scroll stop (better perceived latency, more fling false-positives); higher values waste fewer fetches but arrive later relative to the user’s next tap |
parse and error semantics
The object is parsed unconditionally — including in documents with no origin-trial token — so that a third-party token registered after parsing can still take effect on the next heuristic run without a re-parse; effectiveness is gated separately at application time. Error handling is deliberately forgiving, and the implementation comment states the design rule explicitly: unknown sub-keys and malformed values are ignored; they never cause the whole rule set to fail.
| Input situation | Behavior |
|---|---|
| Key absent | No overrides; the document uses the (Finch-adjustable) default configuration |
| Field absent from the object | That parameter keeps the default; present fields still override (fields are independently optional) |
| Unknown sub-key inside the object | Ignored |
| Wrong type (string, object, boolean…) | That field is ignored; the default applies; the rule set is unaffected |
distance_from_pointer_down not a 2-element number array | Both distance values are ignored; the default band applies |
| Out-of-range numbers | Clamped at application time (per-field rules above) — never a parse error |
context and exposure
- Where it applies. Documents that contain document-source speculation rules with
"eagerness": "moderate", on Android, with theSpeculationRulesModerateViewportHeuristicsControlorigin trial enabled. It does not affect desktop hover-based heuristics, other eagerness levels (immediate,eager,conservative), or list rules (expliciturls), none of which use the viewport candidate picker. - Inline and external rule sets. Parsed by the shared rule-set parser, so the key works in inline
<script type="speculationrules">blocks and external rule-set resources alike. - Prerequisite base behavior. The viewport heuristic itself must be active on the device (the base
kPreloadingModerateViewportHeuristicsfeature behind the Chrome 138 mobile behavior); the overrides layer onto its configuration, they do not enable it. - Third-party contexts. First- and third-party origin-trial tokens both enable the override application, by design, so injected rule sets from performance libraries can participate.
lifecycle
- Parse (page load / rule-set insertion): the object is read into an optional per-rule-set params struct; malformed fields are dropped here, but nothing is clamped or applied yet and no metric is recorded.
- Selection (document level): when the heuristic needs the configuration, the document consults its rule sets in order and uses the first rule set that carries the object; objects on later rule sets are ignored (no merging), a deliberate simplification for an experiment-only mechanism.
- Application (each scroll stop): if and only if the origin trial is enabled for the document, the params are clamped and layered onto the default config; the use counter
kSpeculationRulesModerateViewportHeuristicsControlis recorded at this point so the metric reflects params that actually took effect. - Action: the retuned distance filter, dominance test, and dwell delay govern the next candidate pick; the candidate dies if the user scrolls during the delay, and the delay timer resets on new position updates.
- Teardown: nothing persists — params live and die with the document’s rule sets; navigating away discards all state.
examples
All three fields set (the explainer’s proposal example):
<script type="speculationrules">
{
"moderate_viewport_heuristics": {
"distance_from_pointer_down": [-0.5, 0.2],
"largest_anchor_threshold": 0.1,
"delay": 200
},
"prefetch": [
{ "source": "document", "where": { "href_matches": "/*" }, "eagerness": "moderate" }
]
}
</script>
Conservative experiment — only admit links nearly level with the tap, require a clear size winner, wait longer:
<script type="speculationrules">
{
"moderate_viewport_heuristics": {
"distance_from_pointer_down": [-0.1, 0.1],
"largest_anchor_threshold": 0.5,
"delay": 1500
},
"prefetch": [
{ "source": "document", "where": { "href_matches": "/articles/*" }, "eagerness": "moderate" }
]
}
</script>
Two rule sets — demonstrating first-wins: the second object is ignored, so delay stays 300, not 900:
<script type="speculationrules">
{
"moderate_viewport_heuristics": { "delay": 300 },
"prefetch": [ { "source": "document", "where": { "href_matches": "/*" }, "eagerness": "moderate" } ]
}
</script>
<script type="speculationrules">
{
"moderate_viewport_heuristics": { "delay": 900 },
"prerender": [ { "source": "document", "where": { "href_matches": "/checkout/*" }, "eagerness": "moderate" } ]
}
</script>
Source: first example verbatim from the explainer; first-wins behavior per document_speculation_rules.cc
compatibility
| Engine / browser | Support | Evidence |
|---|---|---|
| Chrome for Android 152–157 | Behind origin trial (first- and third-party tokens); parsed unconditionally, effective only with the token | milestone=152 listing; API-owner LGTM M152–M157; runtime flag record |
| Chrome desktop / WebView | No effect (viewport heuristic is Android-only; trial restricted to Android) | Intent; runtime flag record |
| Firefox / Safari | No signal; key ignored as unknown speculation-rules content | Intent vendor views |
| BCD / webstatus | No entries (checked 2026-07-29) — this table is the interim record | BCD api/; webstatus.dev query |
security and privacy
- The
delayfloor of 10 ms exists “per privacy review” so the heuristic cannot be configured to fire essentially immediately (anchor_element_interaction_tracker.cc). - The parameters influence only the timing and selectivity of speculative loads the browser would be eligible to perform anyway; they introduce no new request destinations, credentials, storage, or identifiers.
- Third-party trial tokens are an explicit design decision to accommodate cross-origin performance libraries (runtime flag record); the parse-anywhere/apply-under-token split exists so such libraries cannot silently change behavior before their token registers (speculation_rule_set.cc).
- Malformed content fails closed: it is ignored, leaving conservative defaults (speculation_rule_set.cc).