Skip to content
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

Suspense for CPU-bound trees #19936

Merged
merged 1 commit into from Sep 30, 2020
Merged

Suspense for CPU-bound trees #19936

merged 1 commit into from Sep 30, 2020

Commits on Sep 30, 2020

  1. Suspense for CPU-bound trees

    Adds a new prop to the Suspense component type,
    `unstable_expectedLoadTime`. The presence of this prop indicates that
    the content is computationally expensive to render.
    
    During the initial mount, React will skip over expensive trees by
    rendering a placeholder — just like we do with trees that are waiting
    for data to resolve. That will help unblock the initial skeleton for the
    new screen. Then we will continue rendering in the next commit.
    
    For now, while we experiment with the API internally, any number passed
    to `unstable_expectedLoadTime` will be treated as "computationally
    expensive", no matter how large or small. So it's basically a boolean.
    The reason it's a number is that, in the future, we may try to be clever
    with this additional information. For example, SuspenseList could use
    it as part of its heuristic to determine whether to keep rendering
    additional rows.
    
    Background
    ----------
    
    Much of our early messaging and research into Suspense focused on its
    ability to throttle the appearance of placeholder UIs. Our theory was
    that, on a fast network, if everything loads quickly, excessive
    placeholders will contribute to a janky user experience. This was backed
    up by user research and has held up in practice.
    
    However, our original demos made an even stronger assertion: not only is
    it preferable to throttle successive loading states, but up to a certain
    threshold, it’s also preferable to remain on the previous screen; or in
    other words, to delay the transition.
    
    This strategy has produced mixed results. We’ve found it works well for
    certain transitions, but not for all them. When performing a full page
    transition, showing an initial skeleton as soon as possible is crucial
    to making the transition feel snappy. You still want throttle the nested
    loading states as they pop in, but you need to show something on the new
    route. Remaining on the previous screen can make the app feel
    unresponsive.
    
    That’s not to say that delaying the previous screen always leads to a
    bad user experience. Especially if you can guarantee that the delay is
    small enough that the user won’t notice it. This threshold is a called a
    Just Noticeable Difference (JND). If we can stay under the JND, then
    it’s worth skipping the first placeholder to reduce overall thrash.
    
    Delays that are larger than the JND have some use cases, too. The main
    one we’ve found is to refresh existing data, where it’s often preferable
    to keep stale content on screen while the new data loads in the
    background. It’s also useful as a fallback strategy if something
    suspends unexpectedly, to avoid hiding parts of the UI that are already
    visible.
    
    We’re still in the process of optimizing our heuristics for the most
    common patterns. In general, though, we are trending toward being more
    aggressive about prioritizing the initial skeleton.
    
    For example, Suspense is usually thought of as a feature for displaying
    placeholders when the UI is missing data — that is, when rendering is
    bound by pending IO.
    
    But it turns out that the same principles apply to CPU-bound
    transitions, too. It’s worth deferring a tree that’s slow to render if
    doing so unblocks the rest of the transition — regardless of whether
    it’s slow because of missing data or because of expensive CPU work.
    
    We already take advantage of this idea in a few places, such as
    hydration. Instead of hydrating server-rendered UI in a single pass,
    React splits it into chunks. It can do this because the initial HTML
    acts as its own placeholder. React can defer hydrating a chunk of UI as
    long as it wants until the user interacts it. The boundary we use to
    split the UI into chunks is the same one we use for IO-bound subtrees:
    the <Suspense /> component.
    
    SuspenseList does something similar. When streaming in a list of items,
    it will occasionally stop to commit whatever items have already
    finished, before continuing where it left off. It does this by showing a
    placeholder for the remaining items, again using the same <Suspense />
    component API, even if the item is CPU-bound.
    
    Unresolved questions
    --------------------
    
    There is a concern that showing a placeholder without also loading new
    data could be disorienting. Users are trained to believe that a
    placeholder signals fresh content. So there are still some questions
    we’ll need to resolve.
    acdlite committed Sep 30, 2020
    Copy the full SHA
    86ecee0 View commit details
    Browse the repository at this point in the history