-
Notifications
You must be signed in to change notification settings - Fork 658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[css-view-transitions-2] Allow synchronous snapshots #9400
Comments
Can you create a representative demo in React that shows this issue? Include where you would perform the synchronous snapshot if you could. This would show the problem, the size of the problem, and the impact of the proposed solution. |
If I understand the problem, is that between the event and the next render opportunity the framework has a gap of time where it could potentially run some operations to prepare for the next state. The problem with the proposed solution is that snapshotting in itself would become an expensive synchronous operation, so it would delay updating the state and this won't add much value... Also the frameworks are often not the only pieces of code that update the DOM on the page, there are 3p libraries, extensions etc. A synchronous DOM render is a radical solution to a problem that can be solved in other ways. The solution we should strive towards is the alternative, something like framework.startViewTransition = () => {
const donePreparing = framework.prepareState();
document.startViewTransition(() => {
await donePreparing;
await framework.commitToDOM()
});
} And yes, frameworks might have to adjust to this, and potentially do something like this internally - likely on the router level, if they want this optimization of "let's start preparing before the render opportunity". |
React's snapshotting callback only works for class components right? They seem pretty discouraged vs function components + hooks. So, even if this was a thing, we're still asking for changes in frameworks, which is "unlikely"? |
It’s still the canonical way to snapshot in React
…On Sat, 23 Sep 2023 at 11:30, Jake Archibald ***@***.***> wrote:
React's snapshotting callback only works for class components right? They
seem pretty discouraged vs function components + hooks.
So, even if this was a thing, we're still asking for changes in
frameworks, which is "unlikely"?
—
Reply to this email directly, view it on GitHub
<#9400 (comment)>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AB34WKXOY6KKEXAJO7WP733X32T33ANCNFSM6AAAAAA5EBUSCU>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
This is true in instances where snapshotting is an expensive operation but compared to render times on an expensive page, it isn't. Like what kind of durations are you seeing or expecting for snapshotting? Because multi-second renders isn't unthinkable and that's work that outside of |
The time gap we're talking about between
So I think I might have misunderstood what problem you're trying to solve here that's not already solved by Think about it this way. With document.startViewTransition(() => {
// after 16ms or less
flushSync(() => {
// This is 2s work, becoming blocking due to flushSync.
updateState();
});
});
// Total blocking: 2016ms With async rendering (what document.startViewTransition(() => {
// after 16ms or less
updateState();
waitForSomeReadyState();
});
});
// Total blocking: <16ms With sync snapshotting: /* This needs to render, so 10ms */
document.startViewTransitionSync();
/* Non-blocking */
updateState();
document.notifyUpdateComplete();
// Total blocking: 10ms */ With preparing work in parallel to waiting for frame: startPreparing();
document.startViewTransition(() => {
// after 16ms or less
updateState();
waitForSomeReadyState();
});
});
// Total blocking: maybe 0?? So considering that you use something like |
@noamr From https://developer.chrome.com/docs/web-platform/view-transitions/#async-dom-updates-and-waiting-for-content
Am I not right in thinking the page is visually frozen from the moment I need to play around with Given this, I am looking to, with this PR, move as much work outside of |
Sandbox example with Run https://y47h74.csb.app/ with 6x CPU throttling enabled to see how this feels. You can change What I'm looking for is to be able to move the bit where we freeze the page as close to the bit where we update the DOM as possible. In this performance screenshot of the sandbox, the red box is how much time visual updates and interactions are blocked and the green box is how much time that could be with some kind of synchronous |
Thanks for the demo! I see that we were talking about two different meanings of "interruptible". About describing the problemIn the first image, there are less long tasks, so the main thread is in fact responsive in that way. That's what What you want to do here is to postpone the call to About the proposed solutionFirst of all, I can't find any mention of these snapshot callbacks in the React documentation. Do you have a link? Is this some deprecated thing? I suspect that the thing with snapshotting here is that the DOM might be updated by someone other than React while React does all this "work" and perhaps updates suspense-fallbacks (loading spinners etc). For example, you have these CSS animations running. If we do some sync snapshotting at the start, and then let animations keep running until we're ready to commit, wouldn't that create a jump? Could be that I'm not entirely getting the sync snapshotting thing yet. I still think that the solution for this is something like /cc @khushalsagar on this. |
Yeah that's a good clear explanation, thanks. Here are some snapshot lifecycles for popular view libraries:
The animation wouldn't keep running, it would be paused for a much shorter amount of time. const transition = document.snapshotViewTransition() // pause
transition.notifyUpdated() // start transition
From my perspective, rather than wait what is probably years for these view libraries to offer good APIs, by making this one API more fungible it could be solved in userland today. In the meantime we're going to have blocked UIs all over the web. |
@mattgperry in the demo, could you include a console log at the point you'd like to call |
Sorry for the delay, have been working further with View Transitions API. There's not a place in the existing code where I would put the call. What I would do is have something like a <PageTransition path={latestUrl}>{content}</PageTransition> This would be a React component that looks like class PageTransition extends React.Component {
getSnapshotBeforeUpdate(prevProps) {
if (this.props.path !== prevProps.path) {
this.transition = document.snapshotViewTransition()
}
}
componentDidUpdate() {
if (this.transition) {
this.transition.notifyUpdated()
}
}
render() {
return this.props.children
}
} An alternative could be adding a method to the const transition = document.startViewTransition()
transition.snapshot()
updateDOM()
transition.notifyUpdated() Or something. I think this small change would make it much more performant in real-world usage across existing view libraries for the reasons stated earlier. |
Sorry for the late reply and thanks for clearly outlining the feature request @mattgperry! The reason why it's hard to make capture synchronous is that we need to run the update the rendering loop for it. Making it synchronous would mean that this loop has to be triggered from within a script call, Browsers generally have the Document's last rendered frame cached. If we only had to snapshot the root, we could've provided that frame as the snapshot (it will be limited to the DOM state from the last rAF). But snapshotting elements independently affects layerization and a bunch of rendering decisions at every step in the pipeline. Its just not feasible to do it outside of a rendering loop. So we have to wait until the next rendering loop after Can you clarify why async snapshotting is hard for frameworks to accommodate. It looks like you want to trigger a synchronous snapshot operation at the end of "big chunk of interruptible work". Since the big chunk of work is meant to be interruptible by design (for interactivity), it should be possible to yield for a frame between "big chunk of interruptible work" and "DOM Update". That gives the browser the opportunity to snapshot at the next frame. The delay after the next frame finishing and the updateCallback getting dispatched should be very less. Maybe I'm missing the complexity on the framework side here. |
Thanks for explaining from the technical side @khushalsagar! It does sound infeasible. I felt like it could be like WAAPI where animated values aren’t resolved synchronously by default - but may be, should certain methods be called, essentially as a de-opt. From the framework side it would - in my ignorant opinion - be relatively easy. But realistically speaking we have five or so popular frameworks that are probably going to move slowly on integrating with a currently narrowly supported API in order to “get it right”. Whereas I’m asking for the tools to solve this in userland so view transitions can be faster for all users today. Already we have libraries like Remix integrating it in a way that is not as performant as it would be with my suggested API available. |
Problem
Currently, view transitions are triggered asynchronously.
Every example of view library integration shows
startViewTransition
wrapping some kind of state update function.The work performed within this state change is often heavy, notably when changing everything on a page (a common usecase for this API). It includes all the code within a render function, everything the view library does to compute the required DOM updates (diffing etc), the update itself, any layout effects etc.
So we are required to freeze the website (animations in most cases, scroll and other interactions) throughout all this work.
The vast majority of this work is, at least in React and probably in other libraries, usually interruptable. But not when we've frozen everything for a view transition.
Proposed solution
A synchronous alternative to snapshotting would allow any view library that supports before/after commit lifecycles to snapshot just before the DOM is updated, and start the animation just after it's commited. Moving all the calculation, diffing etc to before the snapshot.
Although the snapshot itself is now (optionally!) synchronous, moving all this work to before the snapshot means that view libraries like React that support time chunking and interruption will remain responsive. Likewise we will only snapshot changes that are actually going to happen.
Alternatives
It is of course possible that all view libraries that exist or could exist make it possible for the snapshot lifecycle to defer commit until a returned promise is resolved. Something like this?
But it's unlikely any will implement this just for the View Transitions API and practically by offering a synchronous alternative to the current API we can solve this across all view libraries right now.
The text was updated successfully, but these errors were encountered: