-
Notifications
You must be signed in to change notification settings - Fork 46.3k
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
Suspense for CPU-bound trees #19936
Conversation
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.
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 86ecee0:
|
Details of bundled changes.Comparing: 480626a...86ecee0 react-dom
react-art
react-reconciler
react-test-renderer
react-native-renderer
ReactDOM: size: 0.0%, gzip: -0.0% Size changes (experimental) |
Details of bundled changes.Comparing: 480626a...86ecee0 react-dom
react-art
react-native-renderer
react-reconciler
react-test-renderer
ReactDOM: size: 0.0%, gzip: -0.0% Size changes (stable) |
I'm curious how this interacts with the scheduler. My understanding was that really computationally heavy work should use scheduling (whether provided by the platform or by reacts scheduler) in order to not cause jank/starvation... On the other hand that means turning work "async" and/or not CPU bound, so is the expectedLoadTime functionality specifically for CPU heavy work that doesn't use scheduling (because its some existing library and or cpu bound but not extremely heavy)? |
FWIW there do exist UIs where placeholders/loading screens are shown for performance reasons, and not necessarily because new content is being loaded. This is pretty common in video games— there's loading screens when entering/exiting rooms, low-poly meshes shown before high-poly ones load, etc. Very cool work, and very interested to hear how this works out 🙂 |
e.g. profiling tools may also use this pattern. I guess it's still related to "data" but it's not the loading of the data from somewhere else. It's the (CPU) processing of the data already in local state. |
In the enter/exit rooms in games case, there’s not really any staleness issue since you always just get the same one. Profiling data is the same. Even if you throw away the processing and do it again, it’ll still show the same result. In either case it doesn’t matter if you did “refresh” or not since you see the same thing. The stale data problem is more like going back to a story you’ve already seen and not seeing new comments, or tracking info for a package and showing the old state of the delivery. The problem happens because we’re trained to expect a loading indicator when something refreshes and not when it’s showing data from the cache. I think this can be solved with a simple feature, that Royi suggested. Just add another “fallback” option that you can specify as an alternative UI for these cases like “computingFallback={...}”. (The term fallback as probably outlived its purpose tho.) I wonder if this can backfire in two ways though. Not showing an indicator when something loads fast might also indicate that something didn’t refresh even though it did. Plenty of examples of sites getting too fast causing expectation. |
// it behind on this node. | ||
workInProgress.lanes = SomeRetryLane; | ||
if (enableSchedulerTracing) { | ||
markSpawnedWork(SomeRetryLane); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth adding a test case to DebugTracing-test
for this
Not necessarily. Profiling tools can let you load new profiling data (in which case, the "loading" bit happens really fast– but the processing bit is slow, during which time the data you're currently viewing is stale). I dunno. Maybe this comparison isn't really worth going into. Seemed a bit related to me. |
Hi, is there any specific case about this? |
Summary: This sync includes the following changes: - **[eaaf4cbce](facebook/react@eaaf4cbce )**: 17.0.1 //<Dan Abramov>// - **[6f62abb58](facebook/react@6f62abb58 )**: Remove usage of Array#fill ([#20071](facebook/react#20071)) //<Dan Abramov>// - **[40cfe1f48](facebook/react@40cfe1f48 )**: Update CHANGELOG.md //<Dan Abramov>// - **[f021a983a](facebook/react@f021a983a )**: Bump versions for 17 ([#20062](facebook/react#20062)) //<Dan Abramov>// - **[d1bb4d851](facebook/react@d1bb4d851 )**: Profiler: Include ref callbacks in onCommit duration ([#20060](facebook/react#20060)) //<Brian Vaughn>// - **[c59c3dfe5](facebook/react@c59c3dfe5 )**: useRef: Warn about reading or writing mutable values during render ([#18545](facebook/react#18545)) //<Brian Vaughn>// - **[7b6cac952](facebook/react@7b6cac952 )**: Improved Profiler commit hooks test ([#20053](facebook/react#20053)) //<Brian Vaughn>// - **[dfb6a4033](facebook/react@dfb6a4033 )**: [Fast Refresh] Fix crashes caused by rogue Proxies ([#20030](facebook/react#20030)) ([#20039](facebook/react#20039)) //<Kai Riemann>// - **[37cb732c5](facebook/react@37cb732c5 )**: Use bitwise OR to define flag masks ([#20044](facebook/react#20044)) //<Andrew Clark>// - **[eb3181e77](facebook/react@eb3181e77 )**: Add Visibility flag for hiding/unhiding trees ([#20043](facebook/react#20043)) //<Andrew Clark>// - **[0dd809bdf](facebook/react@0dd809bdf )**: Remove last schedulePassiveEffectCallback call ([#20042](facebook/react#20042)) //<Andrew Clark>// - **[8df7b7911](facebook/react@8df7b7911 )**: Remove Passive flag from "before mutation" phase ([#20038](facebook/react#20038)) //<Andrew Clark>// - **[c57fe4a2c](facebook/react@c57fe4a2c )**: ReactIs.isValidElementType Unit Test extended with PureComponent case ([#20033](facebook/react#20033)) //<adasq>// - **[02da938fd](facebook/react@02da938fd )**: Don't double-invoke effects in legacy roots ([#20028](facebook/react#20028)) //<Brian Vaughn>// - **[d95c4938d](facebook/react@d95c4938d )**: [EventSystem] Revise onBeforeBlur propagation mechanics ([#20020](facebook/react#20020)) //<Dominic Gannaway>// - **[f46a80ae1](facebook/react@f46a80ae1 )**: Update outdated links and fix two broken links ([#19985](facebook/react#19985)) //<Saikat Guha>// - **[0a4c7c565](facebook/react@0a4c7c565 )**: [Flight] Don't warn for key, but error for ref ([#19986](facebook/react#19986)) //<Sebastian Markbåge>// - **[993ca533b](facebook/react@993ca533b )**: Enable eager listeners statically ([#19983](facebook/react#19983)) //<Dan Abramov>// - **[40c52de96](facebook/react@40c52de96 )**: [Flight] Add Runtime Errors for Non-serializable Values ([#19980](facebook/react#19980)) //<Sebastian Markbåge>// - **[1992d9730](facebook/react@1992d9730 )**: Revert "Temporarily disable Profiler commit hooks flag ([#19900](facebook/react#19900))" ([#19960](facebook/react#19960)) //<Brian Vaughn>// - **[44d39c4d7](facebook/react@44d39c4d7 )**: Removed skip-error-boundaries modifications from old fork ([#19961](facebook/react#19961)) //<Brian Vaughn>// - **[cc77be957](facebook/react@cc77be957 )**: Remove unnecessary error overriding in ([#19949](facebook/react#19949)) //<Paul Doyle>// - **[97625272a](facebook/react@97625272a )**: Debug tracing tests for CPU bound suspense ([#19943](facebook/react#19943)) //<Brian Vaughn>// - **[43363e279](facebook/react@43363e279 )**: Fix codestyle for typeof comparison ([#19928](facebook/react#19928)) //<Eugene Maslovich>// - **[5427b4657](facebook/react@5427b4657 )**: Temporarily disable Profiler commit hooks flag ([#19900](facebook/react#19900)) //<Brian Vaughn>// - **[1faf9e3dd](facebook/react@1faf9e3dd )**: Suspense for CPU-bound trees ([#19936](facebook/react#19936)) //<Andrew Clark>// - **[7f08e908b](facebook/react@7f08e908b )**: Fix missing context to componentDidMount() when double-invoking lifecycles ([#19935](facebook/react#19935)) //<Brian Vaughn>// - **[9198a5cec](facebook/react@9198a5cec )**: Refactor layout effect methods ([#19895](facebook/react#19895)) //<Brian Vaughn>// - **[ba82eea38](facebook/react@ba82eea38 )**: Remove disableSchedulerTimeoutInWorkLoop flag ([#19902](facebook/react#19902)) //<Andrew Clark>// - **[c63741fb3](facebook/react@c63741fb3 )**: offscreen double invoke effects ([#19523](facebook/react#19523)) //<Luna Ruan>// - **[c6917346f](facebook/react@c6917346f )**: Fixed broken Profiler test ([#19894](facebook/react#19894)) //<Brian Vaughn>// - **[87c023b1c](facebook/react@87c023b1c )**: Profiler onRender only called when we do work ([#19885](facebook/react#19885)) //<Brian Vaughn>// - **[81aaee56a](facebook/react@81aaee56a )**: Don't call onCommit et al if there are no effects ([#19863](facebook/react#19863)) //<Andrew Clark>// - **[7355bf575](facebook/react@7355bf575 )**: Consolidate commit phase hook functions ([#19864](facebook/react#19864)) //<Andrew Clark>// - **[bc6b7b6b1](facebook/react@bc6b7b6b1 )**: Don't trigger lazy in DEV during element creation ([#19871](facebook/react#19871)) //<Dan Abramov>// - **[a774502e0](facebook/react@a774502e0 )**: Use single quotes in getComponentName return ([#19873](facebook/react#19873)) //<Gustavo Saiani>// - **[8b2d3783e](facebook/react@8b2d3783e )**: Use Passive flag to schedule onPostCommit ([#19862](facebook/react#19862)) //<Andrew Clark>// - **[50d9451f3](facebook/react@50d9451f3 )**: Improve DevTools editing interface ([#19774](facebook/react#19774)) //<Brian Vaughn>// - **[6fddca27e](facebook/react@6fddca27e )**: Remove passive intervention flag ([#19849](facebook/react#19849)) //<Dan Abramov>// - **[36df9185c](facebook/react@36df9185c )**: chore(docs): Removed outdated comment about fb.me link ([#19830](facebook/react#19830)) //<Adnaan Bheda>// - **[16fb2b6f9](facebook/react@16fb2b6f9 )**: Moved resetChildLanes into complete work ([#19836](facebook/react#19836)) //<Brian Vaughn>// - **[cc581065d](facebook/react@cc581065d )**: [email protected] //<Dan Abramov>// - **[0044805c8](facebook/react@0044805c8 )**: Update CHANGELOG.md //<Dan Abramov>// - **[0f70d4dd6](facebook/react@0f70d4dd6 )**: Consider components in jsx as missing dependencies in typescript-eslint/[email protected] ([#19815](facebook/react#19815)) //<Sebastian Silbermann>// - **[84558c61b](facebook/react@84558c61b )**: Don't visit passive effects during layout phase ([#19809](facebook/react#19809)) //<Andrew Clark>// - **[ad8a0a8cd](facebook/react@ad8a0a8cd )**: [email protected] //<Dan Abramov>// - **[77544a0d6](facebook/react@77544a0d6 )**: Update CHANGELOG.md //<Dan Abramov>// - **[ed4fdfc73](facebook/react@ed4fdfc73 )**: test(eslint-plugin-react-hooks): Run with TS parsers >= 2.x ([#19792](facebook/react#19792)) //<Sebastian Silbermann>// - **[cd75f93c0](facebook/react@cd75f93c0 )**: eslint-plugin-react-hooks: fix compatibility with typescript-eslint/[email protected]+ ([#19751](facebook/react#19751)) //<Matthias Schiffer>// - **[781212aab](facebook/react@781212aab )**: Remove double space in test name ([#19762](facebook/react#19762)) //<Gustavo Saiani>// - **[e7b255341](facebook/react@e7b255341 )**: Internal `act`: Flush timers at end of scope ([#19788](facebook/react#19788)) //<Andrew Clark>// - **[d17086c7c](facebook/react@d17086c7c )**: Decouple public, internal act implementation ([#19745](facebook/react#19745)) //<Andrew Clark>// - **[d38ec17b1](facebook/react@d38ec17b1 )**: [Flight] Set dispatcher for duration of performWork() ([#19776](facebook/react#19776)) //<Joseph Savona>// - **[4f3f7eeb7](facebook/react@4f3f7eeb7 )**: Bugfix: Effect clean up when deleting suspended tree ([#19752](facebook/react#19752)) //<Andrew Clark>// - **[7baf9d412](facebook/react@7baf9d412 )**: Combine Flags and SubtreeFlags types ([#19775](facebook/react#19775)) //<Andrew Clark>// - **[166544360](facebook/react@166544360 )**: Rename effect fields ([#19755](facebook/react#19755)) //<Andrew Clark>// - **[708fa77a7](facebook/react@708fa77a7 )**: Decrease expiration time of input updates ([#19772](facebook/react#19772)) //<Andrew Clark>// - **[36df483af](facebook/react@36df483af )**: Add feature flag to disable scheduler timeout in work loop ([#19771](facebook/react#19771)) //<Ricky>// - **[bcc0aa463](facebook/react@bcc0aa463 )**: Revert "Revert "Remove onScroll bubbling flag ([#19535](facebook/react#19535))" ([#19655](facebook/react#19655))" ([#19761](facebook/react#19761)) //<Dan Abramov>// - **[99cae887f](facebook/react@99cae887f )**: Add failing test for passive effect cleanup functions and memoized components ([#19750](facebook/react#19750)) //<Brian Vaughn>// - **[2cfd73c4d](facebook/react@2cfd73c4d )**: Fix typo in comment (Noticable→Noticeable) ([#19737](facebook/react#19737)) //<Ikko Ashimine>// - **[53e622ca7](facebook/react@53e622ca7 )**: Fix instances of function declaration after return ([#19733](facebook/react#19733)) //<Bhumij Gupta>// - **[b7d18c4da](facebook/react@b7d18c4da )**: Support Babel's envName option in React Refresh plugin ([#19009](facebook/react#19009)) //<Kevin Weber>// - **[1f38dcff6](facebook/react@1f38dcff6 )**: Remove withSuspenseConfig ([#19724](facebook/react#19724)) //<Andrew Clark>// - **[1396e4a8f](facebook/react@1396e4a8f )**: Fixes eslint warning when node type is ChainExpression ([#19680](facebook/react#19680)) //<Pascal Fong Kye>// - **[a8500be89](facebook/react@a8500be89 )**: Add `startTransition` as a known stable method ([#19720](facebook/react#19720)) //<Andrew Clark>// - **[380dc95de](facebook/react@380dc95de )**: Revert "Append text string to <Text> error message ([#19581](facebook/react#19581))" ([#19723](facebook/react#19723)) //<Timothy Yung>// - **[ddd1faa19](facebook/react@ddd1faa19 )**: Remove config argument from useTransition ([#19719](facebook/react#19719)) //<Andrew Clark>// - **[92fcd46cc](facebook/react@92fcd46cc )**: Replace SuspenseConfig object with an integer ([#19706](facebook/react#19706)) //<Andrew Clark>// - **[b754caaaf](facebook/react@b754caaaf )**: Enable eager listeners in open source ([#19716](facebook/react#19716)) //<Dan Abramov>// - **[c1ac05215](facebook/react@c1ac05215 )**: [Flight] Support more element types and Hooks for Server and Hybrid Components ([#19711](facebook/react#19711)) //<Dan Abramov>// - **[1eaafc9ad](facebook/react@1eaafc9ad )**: Clean up timeoutMs-related implementation details ([#19704](facebook/react#19704)) //<Andrew Clark>// - **[8da0da093](facebook/react@8da0da093 )**: Disable timeoutMs argument ([#19703](facebook/react#19703)) //<Andrew Clark>// - **[60ba723bf](facebook/react@60ba723bf )**: Add SuspenseList to devTools ([#19684](facebook/react#19684)) //<Ben Pernick>// - **[5564f2c95](facebook/react@5564f2c95 )**: Add React.startTransition ([#19696](facebook/react#19696)) //<Ricky>// - **[c4e0768d7](facebook/react@c4e0768d7 )**: Remove unused argument from `finishConcurrentRender` ([#19689](facebook/react#19689)) //<inottn>// - **[848bb2426](facebook/react@848bb2426 )**: Attach Listeners Eagerly to Roots and Portal Containers ([#19659](facebook/react#19659)) //<Dan Abramov>// - **[d2e914ab4](facebook/react@d2e914ab4 )**: Remove remaining references to effect list ([#19673](facebook/react#19673)) //<Andrew Clark>// - **[d6e433899](facebook/react@d6e433899 )**: Use Global Render Timeout for CPU Suspense ([#19643](facebook/react#19643)) //<Sebastian Markbåge>// - **[64ddef44c](facebook/react@64ddef44c )**: Revert "Remove onScroll bubbling flag ([#19535](facebook/react#19535))" ([#19655](facebook/react#19655)) //<Dan Abramov>// - **[dd651df05](facebook/react@dd651df05 )**: Keep onTouchStart, onTouchMove, and onWheel passive ([#19654](facebook/react#19654)) //<Dan Abramov>// - **[b8fa09e9e](facebook/react@b8fa09e9e )**: provide profiling bundle for react-reconciler ([#19559](facebook/react#19559)) //<Julien Gilli>// - **[23595ff59](facebook/react@23595ff59 )**: Add missing param to safelyCallDestroy() ([#19638](facebook/react#19638)) //<Brian Vaughn>// - **[ee409ea3b](facebook/react@ee409ea3b )**: change destroy to safelyCallDestroy ([#19605](facebook/react#19605)) //<Luna Ruan>// - **[bcca5a6ca](facebook/react@bcca5a6ca )**: Always skip unmounted/unmounting error boundaries ([#19627](facebook/react#19627)) //<Brian Vaughn>// - **[1a41a196b](facebook/react@1a41a196b )**: Append text string to <Text> error message ([#19581](facebook/react#19581)) //<Timothy Yung>// - **[e4afb2fdd](facebook/react@e4afb2fdd )**: [email protected] //<Dan Abramov>// - **[ced05c46c](facebook/react@ced05c46c )**: Update CHANGELOG.md //<Dan Abramov>// - **[702fad4b1](facebook/react@702fad4b1 )**: refactor fb.me redirect link to reactjs.org/link ([#19598](facebook/react#19598)) //<CY Lim>// - **[49cd77d24](facebook/react@49cd77d24 )**: fix: leak strict mode with UMD builds ([#19614](facebook/react#19614)) //<Toru Kobayashi>// - **[ffb749c95](facebook/react@ffb749c95 )**: Improve error boundary handling for unmounted subtrees ([#19542](facebook/react#19542)) //<Brian Vaughn>// - **[9b35dd2fc](facebook/react@9b35dd2fc )**: Permanently removed component stacks from scheduling profiler data ([#19615](facebook/react#19615)) //<Brian Vaughn>// - **[3f8115cdd](facebook/react@3f8115cdd )**: Remove `didTimeout` check from work loop //<Andrew Clark>// - **[9abc2785c](facebook/react@9abc2785c )**: Remove wasteful checks from `shouldYield` //<Andrew Clark>// - **[1d5e10f70](facebook/react@1d5e10f70 )**: [eslint-plugin-react-hooks] Report constant constructions ([#19590](facebook/react#19590)) //<Jordan Eldredge>// - **[dab0854c5](facebook/react@dab0854c5 )**: Move commit passive unmount/mount to CommitWork ([#19599](facebook/react#19599)) //<Sebastian Markbåge>// - **[ccb6c3945](facebook/react@ccb6c3945 )**: Remove unused argument ([#19600](facebook/react#19600)) //<inottn>// - **[629125555](facebook/react@629125555 )**: [Scheduler] Re-throw unhandled errors ([#19595](facebook/react#19595)) //<Andrew Clark>// - **[b8ed6a1aa](facebook/react@b8ed6a1aa )**: [Scheduler] Call postTask directly ([#19551](facebook/react#19551)) //<Andrew Clark>// - **[ce37bfad5](facebook/react@ce37bfad5 )**: Remove resolutions from test renderer package.json ([#19577](facebook/react#19577)) //<Dan Abramov>// - **[2704bb537](facebook/react@2704bb537 )**: Add ReactVersion to SchedulingProfiler render scheduled marks ([#19553](facebook/react#19553)) //<Kartik Choudhary>// - **[0c52e24cb](facebook/react@0c52e24cb )**: Support inner component _debugOwner in memo ([#19556](facebook/react#19556)) //<Brian Vaughn>// - **[0cd9a6de5](facebook/react@0cd9a6de5 )**: Parallelize Jest in CI ([#19552](facebook/react#19552)) //<Andrew Clark>// - **[a63893ff3](facebook/react@a63893ff3 )**: Warn about undefined return value for memo and forwardRef ([#19550](facebook/react#19550)) //<Brian Vaughn>// - **[32ff42868](facebook/react@32ff42868 )**: Add feature flag for setting update lane priority ([#19401](facebook/react#19401)) //<Ricky>// - **[5bdd4c8c6](facebook/react@5bdd4c8c6 )**: Remove unused argument from call to jest method ([#19546](facebook/react#19546)) //<Gustavo Saiani>// - **[a5fed98a9](facebook/react@a5fed98a9 )**: Register more node types that are used later as JSXIdentifiers ([#19514](facebook/react#19514)) //<Mateusz Burzyński>// - **[f77c7b9d7](facebook/react@f77c7b9d7 )**: Re-add discrete flushing timeStamp heuristic (behind flag) ([#19540](facebook/react#19540)) //<Dominic Gannaway>// Changelog: [general] [feature] Upgrade to React 17 Reviewed By: cpojer Differential Revision: D24491201 fbshipit-source-id: c947da9dcccbd614e9dc58f3339b63e24829aca7
Just wanted to share my opinion, and highlight a probable special use case for this, hopefully helping the React Team here in some way. We have certain pages in our app, which render 5-6000 DOM nodes with the help of React. Throwing all this at React seems to freeze the UI for a few 100ms, in addition to that scrolling to a certain section on the page with a #id on initial load just does not work. (The content is fetched client-side, so no SSR or prerendering). This is why I created https://www.npmjs.com/package/use-eventual-scroll. It will keep listening to DOM changes, and scroll to the #id element, until a certain amount of time. This is not ideal though, because the amount of time after I should disconnect and stop constantly scrolling can create a bad UX on its own. Suppose everything went smooth, and the DOM has "settled" in about 300ms, while it usually takes, say 1000ms. I eagerly set my disconnect time to 1000ms, but the user starts scrolling after 500ms, only to be scrolled back to #id, until 1000ms has passed. Thanks to this PR (and probably the follow-up work on this feature), I think I might be able to wrap my main section into SuspenseList, and using (I had this prior discussion with Dan https://twitter.com/dan_abramov/status/1315735392436531201, and also asked about this on a previous React Core live AMA with Cassidy from Netlify.) Anyways, this is interesting! I am excited to see how this plays out. |
@balazsorban44 If you've gone to the trouble of writing your own hook to wait for the ID to be added to the DOM, then I'm sure you've probably already considered using a windowing library to limit what React renders to the page but I wanted to ask anyway on the slight chance that you hadn't. 😄 |
@bvaughn Thanks for the reply! I did not mean to add noise to this thread, but I am happy to reply. (also open for discussion on Twitter with the same handle, but I do remember you tried helping me with this problem already) My knowledge of windowing libraries might be outdated, but I do remember I was considering it. If I recall correctly, don't you need some height/width information for this to work? Also, how is the SEO support? What about linking to a section with #id? Will it scroll to that part correctly? I can take "you should explore it yourself" as an answer here. I think the one issue that I could not see how should be resolved was, what if the part I would like to virtualize is in the middle of a layout, not the entire page? The scrollbar will show up in the middle of the page then. |
I guess you're right. We're adding noise to an older thread here. Briefly:
|
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.
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.