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

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Sep 30, 2020

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.
@facebook-github-bot facebook-github-bot added CLA Signed React Core Team Opened by a member of the React Core Team labels Sep 30, 2020
@codesandbox-ci
Copy link

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:

Sandbox Source
React Configuration

@sizebot
Copy link

sizebot commented Sep 30, 2020

Details of bundled changes.

Comparing: 480626a...86ecee0

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.1% +0.1% 909.56 KB 910.38 KB 206.72 KB 206.96 KB NODE_DEV
ReactDOMForked-prod.js 🔺+0.1% 🔺+0.1% 374.55 KB 375.09 KB 69.54 KB 69.59 KB FB_WWW_PROD
react-dom-server.node.development.js 0.0% -0.0% 138.57 KB 138.57 KB 36.58 KB 36.58 KB NODE_DEV
react-dom.production.min.js 🔺+0.1% 🔺+0.1% 122.4 KB 122.57 KB 39.41 KB 39.44 KB NODE_PROD
ReactDOMForked-profiling.js +0.1% +0.1% 388.53 KB 389.06 KB 72 KB 72.06 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% -0.0% 144.73 KB 144.73 KB 36.77 KB 36.77 KB UMD_DEV
react-dom-server.node.production.min.js 0.0% 0.0% 20.66 KB 20.66 KB 7.65 KB 7.65 KB NODE_PROD
react-dom-test-utils.production.min.js 0.0% -0.0% 13.71 KB 13.71 KB 5.32 KB 5.32 KB UMD_PROD
ReactDOMTesting-dev.js +0.1% +0.1% 912.36 KB 913.18 KB 205.47 KB 205.65 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% -0.0% 66.29 KB 66.29 KB 18.84 KB 18.84 KB NODE_DEV
ReactDOMTesting-prod.js 🔺+0.1% 🔺+0.1% 371.74 KB 372.25 KB 70.23 KB 70.28 KB FB_WWW_PROD
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 5.52 KB 5.52 KB 1.84 KB 1.84 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 13.7 KB 13.7 KB 5.27 KB 5.26 KB NODE_PROD
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 5.25 KB 5.25 KB 1.78 KB 1.77 KB UMD_DEV
react-dom-unstable-fizz.node.production.min.js 0.0% -0.3% 1.17 KB 1.17 KB 667 B 665 B NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.3% 1.22 KB 1.22 KB 713 B 711 B UMD_PROD
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 4.78 KB 4.78 KB 1.68 KB 1.68 KB NODE_DEV
react-dom.development.js +0.1% +0.1% 955.77 KB 956.61 KB 209.31 KB 209.57 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.5% 1.01 KB 1.01 KB 618 B 615 B NODE_PROD
react-dom.production.min.js 🔺+0.1% 0.0% 122.23 KB 122.39 KB 40.16 KB 40.17 KB UMD_PROD
react-dom.profiling.min.js +0.1% +0.1% 127.48 KB 127.66 KB 41.79 KB 41.83 KB UMD_PROFILING
ReactDOMForked-dev.js +0.1% +0.1% 969.43 KB 970.31 KB 215.46 KB 215.7 KB FB_WWW_DEV
react-dom.profiling.min.js +0.1% +0.1% 127.84 KB 128.02 KB 41.08 KB 41.12 KB NODE_PROFILING
react-dom-server.browser.production.min.js 0.0% -0.0% 20.34 KB 20.34 KB 7.55 KB 7.55 KB UMD_PROD
ReactDOM-dev.js +0.1% +0.1% 961.58 KB 962.45 KB 214.87 KB 215.09 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.1% 🔺+0.1% 374.33 KB 374.87 KB 69.41 KB 69.47 KB FB_WWW_PROD
react-dom-server.browser.development.js 0.0% -0.0% 137.3 KB 137.3 KB 36.33 KB 36.33 KB NODE_DEV
ReactDOM-profiling.js +0.1% +0.1% 387.49 KB 388.01 KB 71.85 KB 71.92 KB FB_WWW_PROFILING
react-dom-server.browser.production.min.js 0.0% -0.0% 20.24 KB 20.24 KB 7.5 KB 7.5 KB NODE_PROD
ReactDOMServer-dev.js 0.0% -0.0% 141.58 KB 141.58 KB 36.3 KB 36.3 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% -0.0% 71.48 KB 71.48 KB 19.36 KB 19.36 KB UMD_DEV

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-prod.js 🔺+0.6% 🔺+0.2% 233.09 KB 234.46 KB 41.38 KB 41.46 KB FB_WWW_PROD
react-art.development.js +0.1% +0.2% 691.64 KB 692.49 KB 146.6 KB 146.83 KB UMD_DEV
react-art.production.min.js 🔺+0.1% 🔺+0.1% 111.35 KB 111.52 KB 34.6 KB 34.65 KB UMD_PROD
react-art.development.js +0.1% +0.2% 592.78 KB 593.6 KB 128.72 KB 128.94 KB NODE_DEV
react-art.production.min.js 🔺+0.2% 🔺+0.3% 76.24 KB 76.4 KB 23.7 KB 23.77 KB NODE_PROD
ReactART-dev.js +0.1% +0.2% 619.44 KB 620.31 KB 131.61 KB 131.82 KB FB_WWW_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.1% +0.2% 650.27 KB 651.09 KB 138.67 KB 138.91 KB NODE_DEV
react-reconciler-reflection.development.js 0.0% -0.0% 16.61 KB 16.61 KB 4.96 KB 4.95 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.2% 🔺+0.2% 86.18 KB 86.34 KB 26.57 KB 26.61 KB NODE_PROD
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.8 KB 2.8 KB 1.16 KB 1.15 KB NODE_PROD
react-reconciler.profiling.min.js +0.2% +0.2% 91.59 KB 91.77 KB 28.22 KB 28.27 KB NODE_PROFILING

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.1% +0.2% 605.93 KB 606.77 KB 127.64 KB 127.86 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.2% 🔺+0.3% 76.42 KB 76.6 KB 24.06 KB 24.13 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 577.21 KB 578.03 KB 126.17 KB 126.36 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.2% 🔺+0.2% 76.21 KB 76.39 KB 23.75 KB 23.78 KB NODE_PROD
ReactTestRenderer-dev.js +0.1% +0.2% 592.34 KB 593.21 KB 127.31 KB 127.5 KB FB_WWW_DEV
ReactTestRenderer-dev.js +0.1% +0.2% 587 KB 587.88 KB 127.08 KB 127.3 KB RN_FB_DEV
ReactTestRenderer-prod.js 🔺+0.3% 🔺+0.2% 229.31 KB 230.03 KB 41.92 KB 42 KB RN_FB_PROD
ReactTestRenderer-profiling.js +0.3% +0.2% 240.66 KB 241.43 KB 44.06 KB 44.14 KB RN_FB_PROFILING

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.1% +0.2% 677.47 KB 678.34 KB 146.97 KB 147.19 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+0.3% 🔺+0.1% 268.02 KB 268.74 KB 47.91 KB 47.98 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.3% +0.2% 279.5 KB 280.27 KB 50.11 KB 50.19 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.1% +0.2% 658.11 KB 658.99 KB 142.33 KB 142.55 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.3% 🔺+0.2% 261.74 KB 262.45 KB 46.62 KB 46.7 KB RN_OSS_PROD
ReactFabric-profiling.js +0.3% +0.2% 273.29 KB 274.07 KB 48.83 KB 48.91 KB RN_OSS_PROFILING

ReactDOM: size: 0.0%, gzip: -0.0%

Size changes (experimental)

Generated by 🚫 dangerJS against 86ecee0

@sizebot
Copy link

sizebot commented Sep 30, 2020

Details of bundled changes.

Comparing: 480626a...86ecee0

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.1% +0.1% 874.11 KB 874.93 KB 200.21 KB 200.42 KB NODE_DEV
ReactDOMForked-prod.js 🔺+0.1% 🔺+0.1% 385.88 KB 386.42 KB 71.32 KB 71.37 KB FB_WWW_PROD
react-dom-server.node.development.js 0.0% -0.0% 137.06 KB 137.06 KB 36.37 KB 36.37 KB NODE_DEV
react-dom.production.min.js 🔺+0.2% 🔺+0.1% 117.84 KB 118.02 KB 38.05 KB 38.09 KB NODE_PROD
ReactDOMForked-profiling.js +0.1% +0.1% 399.91 KB 400.44 KB 73.72 KB 73.77 KB FB_WWW_PROFILING
react-dom-server.browser.development.js 0.0% -0.0% 143.14 KB 143.14 KB 36.57 KB 36.57 KB UMD_DEV
react-dom-server.node.production.min.js 0.0% -0.0% 20.2 KB 20.2 KB 7.58 KB 7.58 KB NODE_PROD
react-dom-test-utils.production.min.js 0.0% -0.0% 13.7 KB 13.7 KB 5.32 KB 5.31 KB UMD_PROD
ReactDOMTesting-dev.js +0.1% +0.1% 940.69 KB 941.51 KB 210.96 KB 211.15 KB FB_WWW_DEV
react-dom-test-utils.development.js 0.0% -0.0% 66.28 KB 66.28 KB 18.84 KB 18.84 KB NODE_DEV
ReactDOMTesting-prod.js 🔺+0.1% 🔺+0.1% 384.81 KB 385.31 KB 72.39 KB 72.45 KB FB_WWW_PROD
react-dom-test-utils.production.min.js 0.0% -0.0% 13.68 KB 13.68 KB 5.26 KB 5.26 KB NODE_PROD
react-dom.development.js +0.1% +0.1% 918.57 KB 919.42 KB 202.75 KB 202.97 KB UMD_DEV
react-dom.production.min.js 🔺+0.2% 🔺+0.2% 117.74 KB 117.92 KB 38.74 KB 38.82 KB UMD_PROD
react-dom.profiling.min.js +0.2% +0.1% 121.63 KB 121.83 KB 39.96 KB 40.01 KB UMD_PROFILING
ReactDOMForked-dev.js +0.1% +0.1% 995.02 KB 995.89 KB 220.2 KB 220.44 KB FB_WWW_DEV
react-dom.profiling.min.js +0.2% +0.1% 121.91 KB 122.1 KB 39.23 KB 39.26 KB NODE_PROFILING
react-dom-server.browser.production.min.js 0.0% -0.0% 19.88 KB 19.88 KB 7.46 KB 7.46 KB UMD_PROD
ReactDOM-dev.js +0.1% +0.1% 987.16 KB 988.04 KB 219.54 KB 219.77 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.1% 🔺+0.1% 385.59 KB 386.13 KB 71.07 KB 71.14 KB FB_WWW_PROD
react-dom-server.browser.development.js 0.0% -0.0% 135.79 KB 135.79 KB 36.12 KB 36.12 KB NODE_DEV
ReactDOM-profiling.js +0.1% +0.1% 398.79 KB 399.31 KB 73.5 KB 73.56 KB FB_WWW_PROFILING
react-dom-server.browser.production.min.js 0.0% -0.0% 19.78 KB 19.78 KB 7.42 KB 7.42 KB NODE_PROD
ReactDOMServer-dev.js 0.0% -0.0% 145.61 KB 145.61 KB 37.31 KB 37.31 KB FB_WWW_DEV
ReactDOMServer-prod.js 0.0% 0.0% 47.3 KB 47.3 KB 11.04 KB 11.04 KB FB_WWW_PROD
react-dom-test-utils.development.js 0.0% -0.0% 71.47 KB 71.47 KB 19.35 KB 19.35 KB UMD_DEV

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactART-prod.js 🔺+0.6% 🔺+0.2% 240.25 KB 241.59 KB 42.63 KB 42.72 KB FB_WWW_PROD
react-art.development.js +0.1% +0.2% 664.74 KB 665.58 KB 141.6 KB 141.82 KB UMD_DEV
react-art.production.min.js 🔺+0.2% 🔺+0.1% 108.44 KB 108.62 KB 33.75 KB 33.8 KB UMD_PROD
react-art.development.js +0.1% +0.2% 567.14 KB 567.96 KB 123.81 KB 124.01 KB NODE_DEV
react-art.production.min.js 🔺+0.3% 🔺+0.2% 73.39 KB 73.57 KB 22.86 KB 22.9 KB NODE_PROD
ReactART-dev.js +0.1% +0.2% 629.45 KB 630.32 KB 133.63 KB 133.85 KB FB_WWW_DEV

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.1% +0.1% 682.85 KB 683.72 KB 147.83 KB 148.04 KB RN_FB_DEV
ReactFabric-dev.js +0.1% +0.1% 663.5 KB 664.38 KB 143.2 KB 143.41 KB RN_FB_DEV
ReactNativeRenderer-dev.js +0.1% +0.2% 677.45 KB 678.33 KB 146.96 KB 147.18 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.3% 🔺+0.2% 261.69 KB 262.4 KB 46.59 KB 46.67 KB RN_FB_PROD
ReactNativeRenderer-prod.js 🔺+0.3% 🔺+0.2% 268.01 KB 268.73 KB 47.9 KB 47.97 KB RN_OSS_PROD
ReactFabric-profiling.js +0.3% +0.2% 273.24 KB 274.01 KB 48.81 KB 48.89 KB RN_FB_PROFILING
ReactNativeRenderer-profiling.js +0.3% +0.2% 279.48 KB 280.25 KB 50.1 KB 50.18 KB RN_OSS_PROFILING
ReactNativeRenderer-prod.js 🔺+0.3% 🔺+0.2% 267.97 KB 268.68 KB 47.88 KB 47.96 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +0.3% +0.2% 279.44 KB 280.21 KB 50.08 KB 50.16 KB RN_FB_PROFILING
ReactFabric-dev.js +0.1% +0.2% 658.1 KB 658.97 KB 142.32 KB 142.54 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.3% 🔺+0.2% 261.72 KB 262.44 KB 46.61 KB 46.69 KB RN_OSS_PROD
ReactFabric-profiling.js +0.3% +0.2% 273.28 KB 274.05 KB 48.83 KB 48.9 KB RN_OSS_PROFILING

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.1% +0.2% 622.15 KB 622.97 KB 133.18 KB 133.39 KB NODE_DEV
react-reconciler-reflection.development.js 0.0% -0.0% 16.6 KB 16.6 KB 4.95 KB 4.95 KB NODE_DEV
react-reconciler.production.min.js 🔺+0.2% 🔺+0.2% 82.89 KB 83.07 KB 25.67 KB 25.72 KB NODE_PROD
react-reconciler-reflection.production.min.js 0.0% -0.2% 2.79 KB 2.79 KB 1.15 KB 1.14 KB NODE_PROD
react-reconciler.profiling.min.js +0.2% +0.2% 86.93 KB 87.12 KB 26.9 KB 26.95 KB NODE_PROFILING

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +0.1% +0.2% 605.9 KB 606.75 KB 127.63 KB 127.85 KB UMD_DEV
react-test-renderer.production.min.js 🔺+0.2% 🔺+0.3% 76.39 KB 76.58 KB 24.04 KB 24.11 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.1% 577.18 KB 578 KB 126.16 KB 126.34 KB NODE_DEV
react-test-renderer.production.min.js 🔺+0.2% 🔺+0.2% 76.19 KB 76.37 KB 23.73 KB 23.77 KB NODE_PROD
ReactTestRenderer-dev.js +0.1% +0.2% 592.33 KB 593.2 KB 127.3 KB 127.49 KB FB_WWW_DEV
ReactTestRenderer-dev.js +0.1% +0.2% 586.99 KB 587.86 KB 127.07 KB 127.29 KB RN_FB_DEV
ReactTestRenderer-prod.js 🔺+0.3% 🔺+0.2% 229.3 KB 230.01 KB 41.91 KB 41.99 KB RN_FB_PROD
ReactTestRenderer-profiling.js +0.3% +0.2% 240.65 KB 241.42 KB 44.05 KB 44.14 KB RN_FB_PROFILING

ReactDOM: size: 0.0%, gzip: -0.0%

Size changes (stable)

Generated by 🚫 dangerJS against 86ecee0

@acdlite acdlite merged commit 1faf9e3 into facebook:master Sep 30, 2020
@tjallingt
Copy link
Contributor

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)?

@mrkev
Copy link
Contributor

mrkev commented Oct 1, 2020

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.

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 🙂

@bvaughn
Copy link
Contributor

bvaughn commented Oct 1, 2020

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.

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.

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.

@sebmarkbage
Copy link
Collaborator

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);
Copy link
Contributor

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

#19943

@bvaughn
Copy link
Contributor

bvaughn commented Oct 1, 2020

Profiling data is the same. Even if you throw away the processing and do it again, it’ll still show the same result.

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.

@NE-SmallTown
Copy link
Contributor

showing a placeholder without also loading new data could be disorienting.

Hi, is there any specific case about this?

facebook-github-bot pushed a commit to facebook/react-native that referenced this pull request Oct 27, 2020
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
@balazsorban44
Copy link

balazsorban44 commented Nov 19, 2020

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 Suspense for each section with an #id, and indicate to React that it should render Suspense elements from top to bottom. This way, instead of relying on time, I could stop DOM change listening when the section with #id has appeared in the DOM for the first time.

(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.

@bvaughn
Copy link
Contributor

bvaughn commented Nov 20, 2020

@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. 😄

@balazsorban44
Copy link

balazsorban44 commented Nov 20, 2020

@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.

@bvaughn
Copy link
Contributor

bvaughn commented Nov 20, 2020

I guess you're right. We're adding noise to an older thread here. Briefly:

  • Many windowing libs require width/height to work. Some support lazy measuring.
  • SEO support is not something I'm very knowledgable about.
  • Linking to # is hard to answer abstractly. If you can map an ID to an index in your data, just render a slice around that index and scroll to it. (How depends on the windowing library you use.)
  • Windowing can be done for the whole page or a section within the page. (How depends on the library though.)

koto pushed a commit to koto/react that referenced this pull request Jun 15, 2021
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed React Core Team Opened by a member of the React Core Team
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants