#seamless-navTPAC 2026
Dublin, Ireland & online
26–30 October 2026
What we want from navigation on the web: instant response (even on cold starts and cross-origin links) and seamless continuity (no state reset).
Today users pay in one of three ways: MPAs freeze while render-blocked and reset all state on swap; SPAs pay a heavy JS tax and are still slow on cold starts; and full-page prerendering requires a big RAM budget "just in case".
First idea we explored: Two-Phase View Transitions—transitioning immediately to a preview state inside the outgoing page while the destination page loads in the background, then transitioning from that preview to the new document once ready.
deferPageSwapImperative two-phase View Transitions add navigateEvent.deferPageSwap({ historyChange, handler }). When the destination isn't already ready, the outgoing page can run a same-document View Transition into a preview/loading state, register a BFCache/abort restore callback, and hold the cross-document page swap until the transition finishes.
Declarative two-phase View Transitions use @view-transition { navigation: auto; preview: always; }, @navigation (phase: preview), and UA view-transition types (-ua-to-preview, -ua-from-preview) so the outgoing page transitions into a preview state without manual JS or BFCache cleanup callbacks.
Whether imperative or declarative, two-phase View Transitions happen on the old Document. The old page has to know how to render the destination's preview UI, it cannot run on cold starts or cross-origin navigations, and when the new Document finally swaps in, any state or input from the preview phase is wiped out.
Document per routeDeclarative Skeleton Documents flip ownership to the destination route: the browser caches a lightweight skeleton Document for a URL pattern, swaps to it at 0ms when navigation starts (even from the omnibox or cross-origin), and then swaps to the real Document when it finishes loading.
inert skeleton feels brokenPre-rendering a reusable skeleton Document solves cold starts and lets the destination route define its own skeleton. But it introduces a double-swap trap: the user sees an interactive-looking skeleton at 0ms, starts typing in the search bar or scrolling, and 300ms later the real Document swaps in and wipes their interaction!
Instead of two separate Documents (skeleton + full page), paint the cached shell at 0ms and stream the content right into the same Document.
Service Worker App Shells & stream stitching
<head> already closedWhy didn't 2016 Service Worker stream stitching take off? (1) respondWith(new Response(stream)) committed 200 OK and CSP at 0ms before talking to the server—so 302 login redirects, 404s, and CSP changes broke; (2) without out-of-order <template for>, <head> had to close before the body stream started, so page-specific <title> or CSS couldn't go in <head>; (3) v1 cached shells stitched with v2 server fragments; (4) you paid 50-150ms SW wakeup latency and maintained two endpoints per URL; and (5) clicking between items still tore down the Document.
200
&
revision → streams patchesBefore looking at the markup, here is the end-to-end flow: the server sends one normal HTML stream split by <?shellboundary> into a reusable shell prefix and route-specific <template for> patches. On first visit it renders as a normal streaming page and caches the prefix. On cold navigations it paints the cached shell at 0ms and streams the patches after the boundary; on warm navigations it keeps the current Document and applies only the patches after the boundary.
One standard HTML file works everywhere: on first visit it streams normally and caches the shell prefix up to <?shellboundary> (which must be a direct child of <body> or <head>). Out-of-order <template for> patches <head> and <main>, and the browser natively checks revision and status before splicing.
At 0ms, the cached shell paints under its cached CSP. When response headers and <meta name="shell"> arrive: if status is 200 and revision + CSP match, skip the prefix and stream <template for> patches. On 302, 404, or revision/CSP mismatch, fall back to a normal MPA load. Servers can verify even earlier with 103 Early Hints or a lightweight HEAD check.
Document and apply only the patches!Once a page is already split into a reusable shell prefix and route-specific <template for> patches, what happens when the user clicks from /item/1 to /item/2 while that shell is already live? We don't need to swap documents at all—we can use the exact same HTML file for a declarative SPA navigation!
<?shellboundary>, streams patches in placeAdding upgrade=always opts into Navigation Upgrade: when navigating between two URLs with the same active shell, the browser intercepts the navigation into a same-document navigation (like a declarative navigateEvent.intercept()), skips the response prefix up to <?shellboundary>, and streams the <template for> patches into the live Document.
preserve: complex, weaker caching story<template for> in continuation: sameHow does this compare to nearby alternatives? (1) DOM merging / preserve attributes keep islands alive across navigations, but don't give you a 0ms cached shell on cold starts and still require parsing and reconciling the entire outer document; (2) simply parsing only <template for> tags from the stream without <?shellboundary> doesn't tell the browser what prefix to cache as the shell on first visit (or distinguish shell-level templates from route patches); and (3) relying on HTTP content negotiation to return only the shell or only the continuation from the same URL breaks static hosting, creates Vary/CDN cache-poisoning bugs (e.g. serving raw patches on hard reload), and forces servers to maintain two endpoints—whereas in-band <?shellboundary> works from a single cacheable HTML file (with Sec-Active-Shell as an optional byte-saving hint).
Participate: WICG/declarative-partial-updates (progressive-shell-explainer.md)
Discussion topics: (1) Security: path-prefix scoping and matching CSP/COOP/COEP headers (falling back to MPA on mismatch); (2) Session history: Back/Forward and BFCache across upgraded navigations; (3) Analytics & Web Vitals: hard vs. soft navigation reporting; (4) Flash of wrong user state: auth/personalization drift in cached shells; and (5) Integration with Navigation API and View Transitions.
Explainer: https://github.com/WICG/declarative-partial-updates/blob/main/progressive-shell-explainer.md. Traces the journey from two-phase view transitions and declarative skeletons to progressive shells.