Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ type ActionResult<T> =

Nested layouts auto-emit `<!--wj:children:<segment-path>-->` markers; the client router walks both DOMs and replaces only the deepest shared layout's children slot, preserving outer-layout DOM identity. Form submissions ride the same pipeline (`data-no-router` opts out). Wire bytes are minimized via the `X-Webjs-Have` header (the server returns only the divergent fragment); scroll is restored on back/forward. A non-GET `<form>` whose target page exports an `action` is the no-JS write-path (with JS the router applies the response in place: a `422` swaps without reload, a `303` is followed via fetch). A failed navigation recovers in place (a cancelable `webjs:navigation-error` event, else a minimal in-place alert), never a destructive full reload.

The advanced client-router surface is in `agent-docs/advanced.md`: **link prefetch** (on by default, `intent` strategy, per-link `data-prefetch`), **`<webjs-frame>`** partial-swap regions, **View Transitions** (opt-in via `<meta name="view-transition">`, plus `data-webjs-permanent` to persist a live element), and **stream actions** (`<webjs-stream>` element-level updates, #248). Production benefits from HTTP/2 at the edge; `npm run start` speaks plain HTTP/1.1 (put a reverse proxy in front for TLS + HTTP/2).
The advanced client-router surface is in `agent-docs/advanced.md`: **link prefetch** (on by default, device-adaptive default: `intent` on a hover pointer, `viewport` (dwell-gated, cancel-on-scroll-out) on touch, per-link `data-prefetch` override), **`<webjs-frame>`** partial-swap regions, **View Transitions** (opt-in via `<meta name="view-transition">`, plus `data-webjs-permanent` to persist a live element), and **stream actions** (`<webjs-stream>` element-level updates, #248). Production benefits from HTTP/2 at the edge; `npm run start` speaks plain HTTP/1.1 (put a reverse proxy in front for TLS + HTTP/2).

---

Expand Down
41 changes: 29 additions & 12 deletions agent-docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,30 +570,45 @@ affects a cached page.
### Link prefetch

Same-origin in-app links are prefetched speculatively so a click
resolves from a warm cache with no round-trip. On by default with the
`intent` strategy (no per-link opt-in needed), the way Next / Nuxt /
SvelteKit ship auto-prefetch. The prefetch request carries the same
`X-Webjs-Have` header a real navigation sends, so the server returns the
same divergent fragment; that fragment lands in a dedicated prefetch
cache (separate from the back/forward snapshot cache) and `fetchAndApply`
consumes it via `prefetchTake` before falling back to the network.
resolves from a warm cache with no round-trip. On by default (no per-link
opt-in needed), the way Next / Nuxt / SvelteKit ship auto-prefetch. The
prefetch request carries the same `X-Webjs-Have` header a real navigation
sends, so the server returns the same divergent fragment; that fragment
lands in a dedicated prefetch cache (separate from the back/forward
snapshot cache) and `fetchAndApply` consumes it via `prefetchTake` before
falling back to the network.

The default strategy is DEVICE-ADAPTIVE, because one strategy cannot serve
both input modalities. On a hover-capable fine pointer (mouse / trackpad)
the default is `intent` (warm on hover / focus, a real head-start before
the click). On touch the default is `viewport` (warm as links settle
on-screen), because a touch device has no hover and `touchstart` fires at
tap time, too late to front-run the navigation. The modality is detected
with `matchMedia('(hover: hover) and (pointer: fine)')`, not a user-agent
sniff. A per-link `data-prefetch` always overrides the adaptive default.

Per link, set `data-prefetch` (a valid-HTML `data-*` attribute, the shape
SvelteKit and Astro use; Next / Nuxt / Remix use a component prop, which
webjs has no equivalent for since links are plain `<a href>`):

```html
<a href="/dashboard">intent: hover / focus / touch (default)</a>
<a href="/dashboard">adaptive: intent on pointer, viewport on touch (default)</a>
<a href="/dashboard" data-prefetch="intent">hover / focus / touch</a>
<a href="/dashboard" data-prefetch="render">eager on insert</a>
<a href="/dashboard" data-prefetch="viewport">on scroll-into-view</a>
<a href="/dashboard" data-prefetch="none">never</a>
```

Next-style aliases are accepted: `true` = `render`, `auto` = `viewport`,
`false` = `none`. `intent` waits a short dwell (~100ms) after hover/focus
so a pointer passing over a link does not fetch it; `viewport` uses an
IntersectionObserver at threshold 0.5; `render` and `viewport` are
applied by a document scan on enable and after each navigation.
so a pointer passing over a link does not fetch it. `viewport` uses an
IntersectionObserver at threshold 0.5 and waits a ~250ms dwell, cancelled
the instant the link scrolls back out, so a fast scroll through a long
list spends no requests (the same gate Astro / Next / Nuxt / Remix /
TanStack / Turbo apply). On touch, `touchstart` additionally warms the
tapped link itself (a single request for a link about to be navigated).
`render` and `viewport` are applied by a document scan on enable and after
each navigation.

Only internal links qualify, using the same eligibility as a click:
cross-origin, `download`, `target` other than `_self`, non-HTML
Expand All @@ -602,7 +617,9 @@ Opt out with `data-prefetch="none"`, `data-no-prefetch`, or
`rel="external"`. Speculation is bounded by a concurrency cap (excess
requests queue and drain as slots free, rather than being dropped),
in-flight de-dupe, and an LRU + TTL cache, and is disabled entirely under
`Save-Data` or `prefers-reduced-data`. A mutating form submission and
`Save-Data`, `prefers-reduced-data`, or a 2g `effectiveType` connection.
The guiding rule: snappy, but never at the cost of bloating the client
network tab; when the two conflict, the gate under-fetches. A mutating form submission and
`revalidate(url)` both evict the prefetch cache alongside the snapshot
cache, so a fragment prefetched before a mutation is never served stale.

Expand Down
11 changes: 7 additions & 4 deletions docs/app/docs/client-router/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,13 +188,16 @@ revalidate();</pre>
<p>Mutating form submissions (POST / PUT / PATCH / DELETE) clear the cache automatically on success. You only need <code>revalidate()</code> when the mutation happens via JS / RPC and didn't go through a form.</p>

<h2>Link prefetch (on by default)</h2>
<p>Same-origin in-app links are prefetched speculatively, so a click resolves from a warm cache with no round-trip. The default strategy is <strong>intent</strong>: a brief hover, focus, or touch (after a short dwell) fetches the page with the same headers a real navigation sends, and the click then consumes that fragment. No attribute is needed; it is on for every internal <code>&lt;a href&gt;</code>, the way Next, Nuxt, and SvelteKit ship auto-prefetch.</p>
<p>Choose a different strategy per link with <code>data-prefetch</code> (a valid-HTML <code>data-*</code> attribute, since webjs has no Link component). Next-style aliases are accepted:</p>
<pre>&lt;a href="/dashboard"&gt;intent: hover / focus / touch (default)&lt;/a&gt;
<p>Same-origin in-app links are prefetched speculatively, so a click resolves from a warm cache with no round-trip. No attribute is needed; it is on for every internal <code>&lt;a href&gt;</code>, the way Next, Nuxt, and SvelteKit ship auto-prefetch, and the prefetch sends the same headers a real navigation does so the click consumes the fragment.</p>
<p>The default strategy is <strong>device-adaptive</strong>, because one strategy cannot serve both input modalities. On a hover-capable pointer (mouse / trackpad) the default is <strong>intent</strong> (warm on hover or focus, a real head-start before the click). On touch the default is <strong>viewport</strong> (warm as links settle on-screen), because touch has no hover and <code>touchstart</code> fires at tap time, too late to help. The modality is detected with <code>matchMedia('(hover: hover) and (pointer: fine)')</code>, not a user-agent sniff, and a per-link <code>data-prefetch</code> always overrides it.</p>
<p>Choose a strategy per link with <code>data-prefetch</code> (a valid-HTML <code>data-*</code> attribute, since webjs has no Link component). Next-style aliases are accepted:</p>
<pre>&lt;a href="/dashboard"&gt;adaptive: intent on pointer, viewport on touch (default)&lt;/a&gt;
&lt;a href="/dashboard" data-prefetch="intent"&gt;hover / focus / touch&lt;/a&gt;
&lt;a href="/dashboard" data-prefetch="render"&gt;eager, on insert (alias: true)&lt;/a&gt;
&lt;a href="/dashboard" data-prefetch="viewport"&gt;on scroll into view (alias: auto)&lt;/a&gt;
&lt;a href="/dashboard" data-prefetch="none"&gt;never (alias: false)&lt;/a&gt;</pre>
<p>Only internal links qualify, using the same eligibility as a click: cross-origin, <code>download</code>, <code>target</code> other than <code>_self</code>, non-HTML extensions, <code>data-no-router</code>, and pure hash jumps are skipped. Opt out with <code>data-prefetch="none"</code>, <code>data-no-prefetch</code>, or <code>rel="external"</code>. Speculation is bounded (a concurrency cap with a draining queue, in-flight de-dupe, an LRU + TTL cache) and is disabled under <code>Save-Data</code> or <code>prefers-reduced-data</code>. A mutating form submission and <code>revalidate()</code> evict the prefetch cache too, so a fragment prefetched before a mutation is never served stale.</p>
<p>The <strong>viewport</strong> strategy waits a ~250ms dwell before warming and cancels the instant a link scrolls back out, so a fast scroll through a long list spends no requests (the same over-fetch gate Astro, Next, Nuxt, Remix, TanStack, and Turbo apply). On touch, <code>touchstart</code> additionally warms the tapped link itself. The guiding rule is snappy without bloating the network tab: when the two conflict, the gate under-fetches.</p>
<p>Only internal links qualify, using the same eligibility as a click: cross-origin, <code>download</code>, <code>target</code> other than <code>_self</code>, non-HTML extensions, <code>data-no-router</code>, and pure hash jumps are skipped. Opt out with <code>data-prefetch="none"</code>, <code>data-no-prefetch</code>, or <code>rel="external"</code>. Speculation is bounded (a concurrency cap with a draining queue, in-flight de-dupe, an LRU + TTL cache) and is disabled under <code>Save-Data</code>, <code>prefers-reduced-data</code>, or a 2g connection. A mutating form submission and <code>revalidate()</code> evict the prefetch cache too, so a fragment prefetched before a mutation is never served stale.</p>
<p><strong>Prefetch issues a real GET</strong>, so a non-idempotent action (logout, anything that mutates) must be a POST or a <code>&lt;form&gt;</code>, never a GET link. This matches every framework that auto-prefetches. A native <code>&lt;link rel="prefetch"&gt;</code> in the document head is the browser's own mechanism and is left untouched.</p>

<h2>Per-segment loading skeletons</h2>
Expand Down
Loading