Follow-up to #152. The console states net origin offload documents-only and says the fifth term is
missing from both sides of the ledger: the XHR/API calls a page's own scripts make when a rendering
crawler runs it. This issue measures that term so the console can count it instead of stating it.
The caveat is still in the tree (packages/console/README.md): "None of it passes through the plugin,
so the figure is documents-only on both sides … the true net offload for rendering crawlers is higher
than shown."
Status — updated 2026-09-16
Not shipped. #154 implemented all three stages and was closed unmerged; nothing from it is on
main (no hydration_calls, no uncacheableSubrequests, no scriptsStripped, no rendersJs). The
work below stands as designed; only the version plan needed renumbering, because the tags it reserved
were consumed by the #155–#159 release train.
| stage |
was |
now |
| browser |
v1.22.0 |
v1.25.0 (main is 1.24.0) |
| plugin |
v0.65.0 |
v0.68.0 (0.66.1 shipped #106; 0.67.0 is taken by #102's PR #162) |
| console |
v0.13.0 |
v0.14.0 (main is 0.13.0 — went to #159) |
One interaction to fold in before re-implementing: #158's endpoint switch drops the page's own
price/availability XHR from bot renders via a fleet block.urlPatterns entry. That shrinks the
renderer's own k on that deployment and raises the blocked count — which the design already
reports, and which is exactly why blocked is counted separately rather than silently lost.
The quantity
k(page) = same-origin subrequests a page load makes whose response no shared cache would serve —
the calls that reach the origin whoever runs the page. Then, per page-view by a crawler that executes
scripts:
|
origin requests |
| without prerender |
1 + k (document, then the page's calls) |
| cache-served, snapshot without scripts |
0 — k is saved |
| cache-served, snapshot with scripts |
k — incurred |
| proxied origin page |
1 + k — incurred |
| our own render |
1 + k — incurred (the renderer runs the page too) |
Where each piece is measured
Browser (v1.25.0) — the renderer's response hook already sees every same-origin response and
already inspects cache headers for its own resource cache. Add a pure classifier over
(request, response) → uncacheable (explicit: non-GET, no-store/private/no-cache,
Set-Cookie, uncacheable status), cacheable (explicit positive freshness: s-maxage/max-age/
Expires), or unspecified (no freshness info — CDN-default dependent; reported, counted on neither
side). Also count same-origin requests our block list aborted (blocked), so the undercount is
visible. Carry the counts on the attempt and post them with the result as
subrequests: { sameOrigin, cacheable, uncacheable, unspecified, blocked }, plus
scriptsStripped: <postProcess.stripScripts> so the plugin knows whether the stored snapshot can
hydrate at all. Cost: string checks per response; ~5 ints on the wire.
Plugin (prerender-v0.68.0)
PrerenderedPage gains uncacheableSubrequests: Int and scriptsStripped: Boolean (nullable; older
rows read as unknown). Written on store.
render gains series subrequests (method = kind) — a value per posted result, so Σ = mean × count;
this is the render fleet's own k cost.
- Registry:
analytics.bots[].rendersJs (boolean). Defaults true for the documented renderers only —
Googlebot, Google InspectionTool, Bingbot, Applebot, YandexBot. Every AI crawler in the registry runs
nothing and stays unflagged.
- Serve path: for a request from a flagged crawler, emit
hydration_calls (new metric; it needs its own
three slots): path = side (saved | incurred | unknown), method = bot, type = source, value = k.
saved = cache serve of a script-stripped snapshot; incurred = cache serve of a snapshot with
scripts, or any origin serve; unknown = no page record, or a page that predates the upgrade. One
in-memory counter bump; ~30 combo rows/period/node.
Console (prerender-console-v0.14.0) — originLoad() becomes
net = 1 − (proxied + renders + Σk_renders + probes + sitemaps + Σk_incurred) ÷ (arrived + Σk_saved + Σk_incurred),
the exposure tile becomes "script calls: saved X · incurred Y", the unknown count is printed (it decays
over one render cycle after the plugin deploys), and unspecified is shown as the CDN-default caveat.
Order
Every stage tolerates the others being absent (the console falls back to v0.12.0's exposure count; the
plugin ignores fields an older fleet does not post). Browser first so k starts landing on pages;
plugin; console. Plugin + console land in one PR here (the console's catalog guard scans the plugin's
emit sites, so a plugin-only merge would fail the console suite), released as two tags.
Not resource-intensive
Measurement rides on hooks that already run per response; storage is two small nullable fields per page;
the serve path adds one counter bump for rendering-crawler serves. The cost is coordination and the
one-render-cycle warm-up.
Follow-up to #152. The console states net origin offload documents-only and says the fifth term is
missing from both sides of the ledger: the XHR/API calls a page's own scripts make when a rendering
crawler runs it. This issue measures that term so the console can count it instead of stating it.
The caveat is still in the tree (
packages/console/README.md): "None of it passes through the plugin,so the figure is documents-only on both sides … the true net offload for rendering crawlers is higher
than shown."
Status — updated 2026-09-16
Not shipped. #154 implemented all three stages and was closed unmerged; nothing from it is on
main(nohydration_calls, nouncacheableSubrequests, noscriptsStripped, norendersJs). Thework below stands as designed; only the version plan needed renumbering, because the tags it reserved
were consumed by the #155–#159 release train.
mainis 1.24.0)mainis 0.13.0 — went to #159)One interaction to fold in before re-implementing: #158's endpoint switch drops the page's own
price/availability XHR from bot renders via a fleet
block.urlPatternsentry. That shrinks therenderer's own
kon that deployment and raises theblockedcount — which the design alreadyreports, and which is exactly why
blockedis counted separately rather than silently lost.The quantity
k(page)= same-origin subrequests a page load makes whose response no shared cache would serve —the calls that reach the origin whoever runs the page. Then, per page-view by a crawler that executes
scripts:
1 + k(document, then the page's calls)0—kis savedk— incurred1 + k— incurred1 + k— incurred (the renderer runs the page too)Where each piece is measured
Browser (
v1.25.0) — the renderer's response hook already sees every same-origin response andalready inspects cache headers for its own resource cache. Add a pure classifier over
(request, response)→uncacheable(explicit: non-GET,no-store/private/no-cache,Set-Cookie, uncacheable status),cacheable(explicit positive freshness:s-maxage/max-age/Expires), orunspecified(no freshness info — CDN-default dependent; reported, counted on neitherside). Also count same-origin requests our block list aborted (
blocked), so the undercount isvisible. Carry the counts on the attempt and post them with the result as
subrequests: { sameOrigin, cacheable, uncacheable, unspecified, blocked }, plusscriptsStripped: <postProcess.stripScripts>so the plugin knows whether the stored snapshot canhydrate at all. Cost: string checks per response; ~5 ints on the wire.
Plugin (
prerender-v0.68.0)PrerenderedPagegainsuncacheableSubrequests: IntandscriptsStripped: Boolean(nullable; olderrows read as unknown). Written on store.
rendergains seriessubrequests(method = kind) — a value per posted result, so Σ = mean × count;this is the render fleet's own
kcost.analytics.bots[].rendersJs(boolean). Defaultstruefor the documented renderers only —Googlebot, Google InspectionTool, Bingbot, Applebot, YandexBot. Every AI crawler in the registry runs
nothing and stays unflagged.
hydration_calls(new metric; it needs its ownthree slots): path = side (
saved|incurred|unknown), method = bot, type = source, value =k.saved= cache serve of a script-stripped snapshot;incurred= cache serve of a snapshot withscripts, or any origin serve;
unknown= no page record, or a page that predates the upgrade. Onein-memory counter bump; ~30 combo rows/period/node.
Console (
prerender-console-v0.14.0) —originLoad()becomesnet = 1 − (proxied + renders + Σk_renders + probes + sitemaps + Σk_incurred) ÷ (arrived + Σk_saved + Σk_incurred),the exposure tile becomes "script calls: saved X · incurred Y", the
unknowncount is printed (it decaysover one render cycle after the plugin deploys), and
unspecifiedis shown as the CDN-default caveat.Order
Every stage tolerates the others being absent (the console falls back to v0.12.0's exposure count; the
plugin ignores fields an older fleet does not post). Browser first so
kstarts landing on pages;plugin; console. Plugin + console land in one PR here (the console's catalog guard scans the plugin's
emit sites, so a plugin-only merge would fail the console suite), released as two tags.
Not resource-intensive
Measurement rides on hooks that already run per response; storage is two small nullable fields per page;
the serve path adds one counter bump for rendering-crawler serves. The cost is coordination and the
one-render-cycle warm-up.