[{"data":1,"prerenderedAt":565},["ShallowReactive",2],{"navigation":3,"-guide-how-it-works":48,"-guide-how-it-works-surround":560},[4],{"title":5,"path":6,"stem":7,"children":8,"icon":47},"Guide","\u002Fguide","1.guide\u002F1.index",[9,12,17,22,27,32,37,42],{"title":10,"path":6,"stem":7,"icon":11},"Getting started","i-lucide-rocket",{"title":13,"path":14,"stem":15,"icon":16},"Nuxt","\u002Fguide\u002Fnuxt","1.guide\u002F2.nuxt","i-logos-nuxt-icon",{"title":18,"path":19,"stem":20,"icon":21},"Next.js","\u002Fguide\u002Fnext","1.guide\u002F3.next","i-logos-nextjs-icon",{"title":23,"path":24,"stem":25,"icon":26},"How it works","\u002Fguide\u002Fhow-it-works","1.guide\u002F4.how-it-works","i-lucide-workflow",{"title":28,"path":29,"stem":30,"icon":31},"Options","\u002Fguide\u002Foptions","1.guide\u002F5.options","i-lucide-settings",{"title":33,"path":34,"stem":35,"icon":36},"API","\u002Fguide\u002Fapi","1.guide\u002F6.api","i-lucide-code",{"title":38,"path":39,"stem":40,"icon":41},"Privacy","\u002Fguide\u002Fprivacy","1.guide\u002F7.privacy","i-lucide-lock",{"title":43,"path":44,"stem":45,"icon":46},"Benchmarks","\u002Fguide\u002Fbenchmarks","1.guide\u002F8.benchmarks","i-lucide-gauge","i-lucide-book-open",{"id":49,"title":23,"body":50,"description":554,"extension":555,"meta":556,"navigation":557,"path":24,"seo":558,"stem":25,"__hash__":559},"content\u002F1.guide\u002F4.how-it-works.md",{"type":51,"value":52,"toc":543,"icon":26},"minimark",[53,57,61,66,102,105,109,124,127,131,138,148,152,155,220,234,238,293,296,300,310,343,359,363,385,396,400,403,479,494,507,511,539],[54,55],"mermaid",{"code":56},"---\nconfig:\n  theme: base\n  themeVariables:\n    fontFamily: ui-monospace, SFMono-Regular, Menlo, monospace\n    fontSize: 13px\n    actorBkg: transparent\n    actorBorder: \"#6b7280\"\n    actorTextColor: \"#6b7280\"\n    actorLineColor: \"#6b7280\"\n    signalColor: \"#6b7280\"\n    signalTextColor: \"#6b7280\"\n    labelBoxBkgColor: transparent\n    labelBoxBorderColor: \"#6b7280\"\n    labelTextColor: \"#6b7280\"\n    noteBkgColor: transparent\n    noteBorderColor: \"#00d492\"\n    noteTextColor: \"#6b7280\"\n    lineColor: \"#6b7280\"\n---\nsequenceDiagram\n    participant B as Browser\n    participant N as Your Nitro server\n    participant J as TypeSafe Jev\n\n    Note over B: collect candidate links l0, l1, l2 …\u003Cbr\u002F>watch scroll and pointer\n    B->>N: POST state, one request in flight\n    N->>N: validate, rate limit, look in cache\n    N->>J: one request, three questions\n    J-->>N: next, soon, exit\n    N-->>B: ids and probabilities\n    Note over B: policy: thresholds, budgets, guards\n    B->>B: effector A, speculation rules, for document navigations\n    B->>B: effector B, Nuxt preloading, for in-app navigations\n",[58,59,60],"p",{},"Every failure along that path ends the same way: an empty prediction, and a page that is\nexactly as it would have been without the module.",[62,63,65],"h2",{"id":64},"_1-candidates","1. Candidates",[58,67,68,69,73,74,77,78,81,82,85,86,89,90,93,94,97,98,101],{},"Every ",[70,71,72],"code",{},"a[href]"," with a box is read from the DOM after hydration, after each navigation, and\nafter a debounced DOM mutation. A link becomes a candidate when it is same-origin, is not the\ncurrent page, is not a ",[70,75,76],{},"download",", is not ",[70,79,80],{},"data-precog=\"off\"",", and passes ",[70,83,84],{},"include"," and\n",[70,87,88],{},"exclude",". Survivors are deduped by path, sorted (hinted first, then visible, then document\norder), capped at ",[70,91,92],{},"maxCandidates",", and given opaque ids: ",[70,95,96],{},"l0",", ",[70,99,100],{},"l1",", and so on.",[58,103,104],{},"The ids matter. Jev is asked to pick one of them, so the model's whole output space is a set\nof strings the server wrote itself. Anchor text goes into the request only as the description\nof an option, and the server strips backticks and newlines from it first.",[62,106,108],{"id":107},"_2-signals","2. Signals",[58,110,111,112,115,116,119,120,123],{},"Alongside the links the client sends a small state: the page path, title, ",[70,113,114],{},"h1"," and\ndescription; the last five paths; time on page, scroll depth and scroll velocity; the pointer\nposition, velocity, the three links it is heading towards and the one it is over; and\n",[70,117,118],{},"saveData"," and ",[70,121,122],{},"effectiveType",".",[58,125,126],{},"Everything is rounded and clamped. Positions are tenths of the viewport, not pixels. A full\nstate with thirty candidates is a few kilobytes.",[62,128,130],{"id":129},"_3-timing","3. Timing",[58,132,133,134,137],{},"A prediction is scheduled when a route finishes, when a scroll settles for 150 ms, when the\npointer changes which link it is closing on, and when the DOM changes. ",[70,135,136],{},"minIntervalMs"," keeps\nthose from piling up, one request is in flight at a time, and starting a new one aborts the\nold one. A response whose candidate list no longer matches the page is thrown away.",[58,139,140,141,143,144,147],{},"Nothing runs before hydration, while the tab is hidden, while the document is itself being\nprerendered, on ",[70,142,118],{},", on a ",[70,145,146],{},"2g"," connection, or before consent when you ask for it.",[62,149,151],{"id":150},"_4-the-questions","4. The questions",[58,153,154],{},"One request, three questions, evaluated in parallel:",[156,157,158,174],"table",{},[159,160,161],"thead",{},[162,163,164,168,171],"tr",{},[165,166,167],"th",{},"Key",[165,169,170],{},"Type",[165,172,173],{},"Asked",[175,176,177,194,207],"tbody",{},[162,178,179,185,191],{},[180,181,182],"td",{},[70,183,184],{},"next",[180,186,187,188],{},"choice over the candidate ids plus ",[70,189,190],{},"none",[180,192,193],{},"Which of these links will the visitor click next, if any?",[162,195,196,201,204],{},[180,197,198],{},[70,199,200],{},"soon",[180,202,203],{},"score over four levels",[180,205,206],{},"How soon will the visitor open another page?",[162,208,209,214,217],{},[180,210,211],{},[70,212,213],{},"exit",[180,215,216],{},"yes\u002Fno",[180,218,219],{},"Will the visitor leave the site entirely instead?",[58,221,222,223,226,227,229,230,233],{},"The click probability of a link is ",[70,224,225],{},"next.probabilities[id]",", discounted by ",[70,228,213],{},". ",[70,231,232],{},"soon.ratio","\ndecides whether a prerender is worth its cost.",[62,235,237],{"id":236},"_5-the-policy","5. The policy",[156,239,240,250],{},[159,241,242],{},[162,243,244,247],{},[165,245,246],{},"Condition",[165,248,249],{},"Action",[175,251,252,272,285],{},[162,253,254,266],{},[180,255,256,97,259,262,263],{},[70,257,258],{},"p >= thresholds.prerender",[70,260,261],{},"soon >= 0.5",", mode allows, not ",[70,264,265],{},"target=\"_blank\"",[180,267,268,269],{},"prerender, up to ",[70,270,271],{},"maxPrerender",[162,273,274,279],{},[180,275,276],{},[70,277,278],{},"p >= thresholds.prefetch",[180,280,281,282],{},"prefetch, up to ",[70,283,284],{},"maxPrefetch",[162,286,287,290],{},[180,288,289],{},"otherwise",[180,291,292],{},"nothing",[58,294,295],{},"A link that is already being speculated keeps its slot while it stays above three quarters of\nits threshold, so a wobbling probability does not cancel and restart the same load.",[62,297,299],{"id":298},"_6-the-two-effectors","6. The two effectors",[58,301,302,303,119,306,309],{},"Nuxt navigations are handled by the client router, which is not a document navigation, so\ndocument speculation rules do nothing for them. This is measured, not assumed: see\n",[70,304,305],{},"e2e\u002Fspa-vs-document.spec.ts",[70,307,308],{},".github\u002FDECISIONS.md",". Hence two effectors.",[58,311,312,316,317,320,321,97,324,119,327,330,331,334,335,338,339,342],{},[313,314,315],"strong",{},"A. Speculation rules."," One ",[70,318,319],{},"\u003Cscript type=\"speculationrules\" data-precog>",", replaced on every\ndecision, with ",[70,322,323],{},"source: \"list\"",[70,325,326],{},"eagerness: \"immediate\"",[70,328,329],{},"tag: \"precog\""," so the loads can\nbe told apart by the ",[70,332,333],{},"Sec-Speculation-Tags"," request header. Without the API, prefetch falls\nback to ",[70,336,337],{},"\u003Clink rel=\"prefetch\">"," and prerender is skipped. This is what makes a full document\nnavigation fast: the first landing on a site, a link opened in a new tab, and the experimental\n",[70,340,341],{},"documentNavigation"," mode.",[58,344,345,348,349,119,352,355,356,123],{},[313,346,347],{},"B. Nuxt preloading."," ",[70,350,351],{},"preloadPayload",[70,353,354],{},"preloadRouteComponents"," for the chosen routes, in\nprobability order. This is where the in-app win comes from, and only for routes that have a\npayload, which means prerendered routes with ",[70,357,358],{},"experimental.payloadExtraction",[62,360,362],{"id":361},"_7-the-server","7. The server",[58,364,365,368,369,372,373,376,377,380,381,384],{},[70,366,367],{},"POST {endpoint}"," rebuilds a trusted state from the body rather than trusting its shape: paths\nmust be rooted and same-origin, candidate ids must be exactly ",[70,370,371],{},"l\u003Cindex>",", numbers are clamped,\nand anything else is rejected with a reason in ",[70,374,375],{},"x-precog-reason",". Cross-site callers are\nrefused. Calls are rate limited per IP with a token bucket, then looked up in Nitro storage\nunder a key built from the page, the link set, and coarse buckets of the signals. The\n",[70,378,379],{},"precog:predict"," hook can answer instead of Jev; ",[70,382,383],{},"precog:predicted"," sees every fresh answer.",[58,386,387,388,391,392,395],{},"Any failure is a ",[70,389,390],{},"503"," with an empty prediction. The client then applies ",[70,393,394],{},"fallback"," and the\npage is untouched.",[62,397,399],{"id":398},"prerendering-has-side-effects","Prerendering has side effects",[58,401,402],{},"A prerendered page runs its JavaScript before the visitor ever asks for it. Gate anything with\na side effect:",[404,405,410],"pre",{"className":406,"code":407,"language":408,"meta":409,"style":409},"language-ts shiki shiki-themes github-light github-dark github-dark","if (document.prerendering) {\n  document.addEventListener(\"prerenderingchange\", start, { once: true });\n} else {\n  start();\n}\n","ts","",[70,411,412,425,452,464,473],{"__ignoreMap":409},[413,414,417,421],"span",{"class":415,"line":416},"line",1,[413,418,420],{"class":419},"so5gQ","if",[413,422,424],{"class":423},"slsVL"," (document.prerendering) {\n",[413,426,428,431,435,438,442,445,449],{"class":415,"line":427},2,[413,429,430],{"class":423},"  document.",[413,432,434],{"class":433},"shcOC","addEventListener",[413,436,437],{"class":423},"(",[413,439,441],{"class":440},"sfrk1","\"prerenderingchange\"",[413,443,444],{"class":423},", start, { once: ",[413,446,448],{"class":447},"suiK_","true",[413,450,451],{"class":423}," });\n",[413,453,455,458,461],{"class":415,"line":454},3,[413,456,457],{"class":423},"} ",[413,459,460],{"class":419},"else",[413,462,463],{"class":423}," {\n",[413,465,467,470],{"class":415,"line":466},4,[413,468,469],{"class":433},"  start",[413,471,472],{"class":423},"();\n",[413,474,476],{"class":415,"line":475},5,[413,477,478],{"class":423},"}\n",[58,480,481,482,485,486,489,490,493],{},"This is why ",[70,483,484],{},"mode"," is ",[70,487,488],{},"'prefetch'"," by default and ",[70,491,492],{},"budget.maxPrerender"," is 1.",[58,495,496,497,500,501,504,505,123],{},"A speculative load is also a real request to your own server, with the visitor's cookies and a\n",[70,498,499],{},"Sec-Purpose: prefetch"," header. Anything with a side effect on ",[70,502,503],{},"GET"," belongs in ",[70,506,88],{},[62,508,510],{"id":509},"browser-support","Browser support",[512,513,514,521,528],"ul",{},[515,516,517,518,520],"li",{},"Speculation rules are Chromium only. Elsewhere prefetch falls back to\n",[70,519,337],{}," and prerender is skipped. The Nuxt preloading effector works\neverywhere.",[515,522,523,524,527],{},"Chrome allows 50 concurrent prefetches and 10 prerenders for ",[70,525,526],{},"immediate"," list rules. The\ndefault budgets are far below that.",[515,529,530,531,534,535,538],{},"Inline rules need ",[70,532,533],{},"'inline-speculation-rules'"," in your ",[70,536,537],{},"script-src"," if you set a CSP.",[540,541,542],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .so5gQ, html code.shiki .so5gQ{--shiki-light:#D73A49;--shiki-default:#F97583;--shiki-dark:#F97583}html pre.shiki code .slsVL, html code.shiki .slsVL{--shiki-light:#24292E;--shiki-default:#E1E4E8;--shiki-dark:#E1E4E8}html pre.shiki code .shcOC, html code.shiki .shcOC{--shiki-light:#6F42C1;--shiki-default:#B392F0;--shiki-dark:#B392F0}html pre.shiki code .sfrk1, html code.shiki .sfrk1{--shiki-light:#032F62;--shiki-default:#9ECBFF;--shiki-dark:#9ECBFF}html pre.shiki code .suiK_, html code.shiki .suiK_{--shiki-light:#005CC5;--shiki-default:#79B8FF;--shiki-dark:#79B8FF}",{"title":409,"searchDepth":427,"depth":427,"links":544},[545,546,547,548,549,550,551,552,553],{"id":64,"depth":427,"text":65},{"id":107,"depth":427,"text":108},{"id":129,"depth":427,"text":130},{"id":150,"depth":427,"text":151},{"id":236,"depth":427,"text":237},{"id":298,"depth":427,"text":299},{"id":361,"depth":427,"text":362},{"id":398,"depth":427,"text":399},{"id":509,"depth":427,"text":510},"From a page full of links to two or three warmed navigations, and what happens at each step.","md",{"icon":26},{"icon":26},{"title":23,"description":554},"P3NVJNRLygx7RnwocEmWoO7uTpP-3mnE6qCMSTtIOgA",[561,563],{"title":18,"path":19,"stem":20,"description":562,"icon":21,"children":-1},"next-precog is a route handler plus a provider, for the App Router.",{"title":28,"path":29,"stem":30,"description":564,"icon":31,"children":-1},"Everything under the precog key in nuxt.config.ts.",1789942050300]