|
1 | | -# Two-phase view transition |
2 | | -Making cross-document view-transitions feel instant and seamless. |
| 1 | +# Two-phase cross-document navigation |
| 2 | +Making the navigation experience customizable & declarative. |
3 | 3 |
|
4 | | -See whatwg/html#10616 |
| 4 | +See whatwg/html#10616, whatwg/html#11819 and w3c/csswg-drafts#12829. |
5 | 5 |
|
6 | | -# Current state |
7 | | -View-transitions don’t start immediately - rendering is paused until the new state is ready. |
8 | | -This is especially visible in cross-document view transitions, where the transition only captures the old when the new document’s response arrives, and starts when the new document is ready to render. |
| 6 | +# Overview |
| 7 | +The period between initiating a navigation (e.g. clicking a link) and consuming the content of the next page ([FCP](https://web.dev/articles/fcp)), is a sensitive moment in user experince. |
| 8 | +It is a point in time where users are very likely to notice delays, jarring moments of frozen display, and abrupt changes to presentation. |
9 | 9 |
|
10 | | -This can create a jarring or abrupt experience. |
| 10 | +The core of this being difficult stems from a tradeoff between speed and smoothness. |
| 11 | +The new document becomes activate ASAP (in favor of speed), at the moment the headers are received. |
| 12 | +However, it cannot render until all of its render-blocking resources and elements are present (in favor of smoothness, preventing FoUCs). |
11 | 13 |
|
12 | | -# Current workarounds |
| 14 | +This "uncanny valley" where the old page is no longer active but the new page cannot render anything is far from being an optimal user experience, and the knobs given for developers to control it are crude and implicit. |
| 15 | + |
| 16 | +# Current knobs |
| 17 | + |
| 18 | +## Cross-document view transitions |
| 19 | + |
| 20 | +This feature declaratively makes the navigation smoother. However, the view-transition starts very late. It captures the old state when the new page's response headers arrive, and then freezes the presentation until rendering is unblocked. |
13 | 21 |
|
14 | 22 | ## Starting an animation manually on the old page |
15 | | -This is possible, however the old page cannot delay the navigation, which means the animation is likely to be interrupted when the navigation commits. |
| 23 | +This would create an instant response to the navigation, however the animation would be interrupted as soon as the new page's headers arrive, freezing at that point. So by default this would be both abrupt and jarring. |
16 | 24 |
|
17 | 25 | ## Intercepting the navigation and restarting it when the animation ends |
18 | | -This works but is hard to achieve without slowing down the whole navigation process. |
| 26 | +This would feel smoother but slows down the whole navigation and tweaking it correctly is finicky. |
19 | 27 |
|
20 | 28 | ## Timing out render-blocking |
21 | | -This can reduce the “frozen” time, however it means the transition doesn’t end at the optimal state, and also doesn’t help with making it feel instant. |
| 29 | +This can reduce the jarring time, however it means the transition doesn’t end at the optimal state, and also doesn’t help with making it feel instant. |
22 | 30 |
|
23 | | -# Proposed solution |
24 | | -The core of the problem is that view-transitions require a start and an end phase before starting, but we don’t know the end phase in advance when it is computed in an async function (e.g. in a navigation). |
| 31 | +# Two-phase transition |
25 | 32 |
|
26 | | -## In a nutshell |
27 | | -Proposing to prototype a “two-phase” view-transition: |
28 | | -Instantly start a transition to a state that can be computed quickly enough or synchronously. Call this a “preview” state. |
29 | | -When the to-preview transition ends, transition from there to the final state. |
30 | | -Only interrupt the first transition after a timeout, otherwise stall the navigation commit until finished. |
31 | | -This should ideally not delay the LCP / loading experience of the new page, as the content keeps loading (and potentially prerendering) in the background. |
| 33 | +To make this part of the experience feel more seamless, developers should be able to create a "two-phase page transition". |
| 34 | +This transition starts *instantly* after navigation initiation (link click), and continues *smoothly* until the next page is ready to render. |
| 35 | +The instant part of the transition can only use information knows to the old page, which could be a skeleton of the new page or something generic of sorts. |
32 | 36 |
|
33 | | -## Phase 1: script-invoked preview |
34 | | -We can create this kind of seamless/instant experience without any new CSS, and potentially without needing to fully spec it normatively by changing the behavior as follows: |
35 | | -Calling document.startViewTransition while there is an uncommitted navigation currently works, however it might get cancelled if the navigation is committed. |
36 | | -Instead, if setting up that preview transition’s new state is fast enough so that it is activated before commit, let the animation run its course and use the final state as the “old” state for the cross-document view-transition. |
| 37 | +To achieve that, there are 3(?) potential avenues |
37 | 38 |
|
38 | | -## Phase 2: declarative, using route-matching |
39 | | -Instead of relying on carefully crafted scripts, use the proposed [declarative routing feature](https://github.com/WICG/declarative-partial-updates/blob/main/route-matching-explainer.md), and compute the preview value declaratively and synchronously by applying the style associated with the new route and using it as the intermediate state. |
| 39 | +## Heuristic-based |
| 40 | +Allow a subset of animations, e.g. ones that started after the navigation was initiated, to continue until the new page is ready to commit. |
| 41 | +This would allow instant reactions to a navigation while not creating the abrupt experience of spotting it prematurely. |
40 | 42 |
|
41 | | -Something like this, though perhaps the “preview” opt-in is not necessary and we can make this inferred |
| 43 | +## Low-level knobs with prerendering support |
| 44 | +Currently, deferring the commit, even for same-origin navigation, is not possible. So the browser is responsible for the handover, |
| 45 | +not allowing the developer to curate this experience. |
42 | 46 |
|
43 | | -@route (to: article) { |
44 | | - .article-skeleton { display: block } |
45 | | -} |
| 47 | +### Deferring commit |
| 48 | +Something like `navigateEvent.waitUntil(promise)` (or `defer` or some such) can let the developer tweak the handover point. |
| 49 | +This can of course also be a footgun as it's a simple way to delay navigations, however it's arguably less of a footgun than the current workarounds. |
| 50 | + |
| 51 | +### Responding to prerender |
| 52 | +When prerendering takes place, a more sensible time to hand over the control to the new document is when it is ready to produce a frame (all the render-blocking resources had been discovered). |
| 53 | +However, it is not guaranteed that the destination page is prerendered, and there is no hook to know when the new page is ready to render. |
| 54 | + |
| 55 | +A rather low level way to expose this is `navigateEvent.prerender()` which initiates a prerender if that hasn't happened yet, and returns a promise that resolves at that point, and compose it with the `waitUntil` method above. |
| 56 | +It is also possible to short-circuit this and somehow declare "please defer commit until prerender", which is perhaps safer than a general-purpose promise-based API. |
| 57 | + |
| 58 | +## Declarative preview transitions |
46 | 59 |
|
| 60 | +The above knobs might be very effective, but might also require expertise to get right. |
| 61 | + |
| 62 | +```css |
47 | 63 | @view-transition { |
48 | 64 | navigation: preview; |
| 65 | + types: skeleton; |
| 66 | +} |
| 67 | + |
| 68 | +:active-view-transition-types(skeleton) { |
| 69 | +/* style the transition here */ |
49 | 70 | } |
| 71 | +``` |
| 72 | + |
| 73 | +This is especially expressive together with route-matching: |
50 | 74 |
|
51 | | -The big advantage of doing this declaratively is that the author doesn't have to worry about "cleaning up after themselves", e.g. in the case of restoring from BFCache. |
52 | | -Since routing is declarative, the style of the "new" route would simply not apply when restoring the "old" page from BFCache because the user is no longer navigating to it. |
| 75 | +```css |
| 76 | +@route (from: movie-list) and (to: movie-details) { |
| 77 | + @view-transition { |
| 78 | + navigation: preview; |
| 79 | + types: skeleton; |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +@route (movie-details) { |
| 84 | + :active-view-transition-types(skeleton) { |
| 85 | + /* style the page as a details page skeleton even if we don't have all the data */ |
| 86 | + } |
| 87 | +} |
| 88 | +``` |
53 | 89 |
|
| 90 | +# Conclusion |
| 91 | +Proposing to pursue both the declarative and JS-based approach for completeness (one for ease of use, one for fine-tuning and complete control), and avoid the heuristic approach as it's a bit opaque and implicit. |
54 | 92 |
|
0 commit comments