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

Add withSuspenseConfig API #15593

Merged
merged 14 commits into from
May 16, 2019
Merged

Conversation

sebmarkbage
Copy link
Collaborator

@sebmarkbage sebmarkbage commented May 9, 2019

This adds unstable_withSuspenseConfig(callback, config) to the react package. This is a primitive that can be used to implement "busy spinners". I recommend reviewing the code commit by commit.

This sets the config as a global state available to all React renderers during the scope of the synchronously executed callback (first arg). If you don't pass a config, it disables suspense (for longer than the default JND).

If any updates are scheduled (render, setState, dispatch) during this scope, then they get this "suspense config" associated with the update.

If during a render pass, we process one of those updates, we mark this render pass as having a "suspense config".

If during a render pass, we are forced to turn already visible content back into a fallback, we mark this render as suspended with a delay if possible. (This doesn't not happen during initial render of new content.)

If we have both marked this render as having a suspense config and it was suspended with delay, then we extend the suspended time beyond JND all the way to the timeout defined in the suspense config.

The suspense config has these options:

{
  timeoutMs: number,
  loadingDelayMs?: number,
  minLoadingDurationMs?: number,
}

The last two are used when we have actually completed a render fairly quickly. If we have exceeded loadingDelayMs but have not yet exceeded loadingDelayMs + minLoadingDurationMs then we suspend the commit until that time even though we could commit early. This can be used to implement busy spinners that delay when they first show but want to stay visible for a minimum time if they do show, to avoid flickering.

Tasks:

  • Implementation
  • Tests

@sizebot
Copy link

sizebot commented May 9, 2019

React: size: 🔺+0.4%, gzip: 🔺+0.3%

ReactDOM: size: 🔺+0.9%, gzip: 🔺+0.8%

Details of bundled changes.

Comparing: 1160b37...4347de6

react

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react.development.js +0.6% +0.6% 108.56 KB 109.19 KB 27.28 KB 27.45 KB UMD_DEV
react.production.min.js 🔺+0.4% 🔺+0.3% 12.25 KB 12.3 KB 4.78 KB 4.79 KB UMD_PROD
react.profiling.min.js +0.3% +0.3% 14.4 KB 14.45 KB 5.3 KB 5.32 KB UMD_PROFILING
react.development.js +0.9% +1.0% 70.17 KB 70.79 KB 18.15 KB 18.32 KB NODE_DEV
react.production.min.js 🔺+2.5% 🔺+2.3% 6.41 KB 6.57 KB 2.66 KB 2.72 KB NODE_PROD
React-dev.js +0.9% +1.1% 68.28 KB 68.9 KB 17.41 KB 17.61 KB FB_WWW_DEV
React-prod.js 🔺+2.4% 🔺+2.2% 16.18 KB 16.57 KB 4.28 KB 4.37 KB FB_WWW_PROD
React-profiling.js +2.4% +2.2% 16.18 KB 16.57 KB 4.28 KB 4.37 KB FB_WWW_PROFILING

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.6% +0.5% 832.25 KB 837.38 KB 190 KB 190.99 KB UMD_DEV
react-dom.production.min.js 🔺+0.9% 🔺+0.8% 104.21 KB 105.1 KB 33.84 KB 34.13 KB UMD_PROD
react-dom.profiling.min.js +0.8% +1.0% 107.36 KB 108.26 KB 34.81 KB 35.15 KB UMD_PROFILING
react-dom.development.js +0.6% +0.5% 826.57 KB 831.7 KB 188.45 KB 189.43 KB NODE_DEV
react-dom.production.min.js 🔺+0.9% 🔺+0.9% 104.2 KB 105.1 KB 33.28 KB 33.59 KB NODE_PROD
react-dom.profiling.min.js +0.8% +0.9% 107.55 KB 108.44 KB 34.18 KB 34.5 KB NODE_PROFILING
ReactDOM-dev.js +0.7% +0.6% 851.16 KB 856.83 KB 189.9 KB 190.96 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+1.2% 🔺+1.0% 348.74 KB 352.99 KB 64.63 KB 65.3 KB FB_WWW_PROD
ReactDOM-profiling.js +1.1% +1.0% 353.92 KB 357.98 KB 65.63 KB 66.3 KB FB_WWW_PROFILING
react-dom-unstable-fire.development.js +0.6% +0.5% 832.59 KB 837.72 KB 190.15 KB 191.14 KB UMD_DEV
react-dom-unstable-fire.production.min.js 🔺+0.9% 🔺+0.8% 104.22 KB 105.12 KB 33.85 KB 34.13 KB UMD_PROD
react-dom-unstable-fire.profiling.min.js +0.8% +1.0% 107.38 KB 108.27 KB 34.82 KB 35.16 KB UMD_PROFILING
react-dom-unstable-fire.development.js +0.6% +0.5% 826.91 KB 832.04 KB 188.59 KB 189.57 KB NODE_DEV
react-dom-unstable-fire.production.min.js 🔺+0.9% 🔺+0.9% 104.21 KB 105.11 KB 33.29 KB 33.6 KB NODE_PROD
react-dom-unstable-fire.profiling.min.js +0.8% +0.9% 107.56 KB 108.46 KB 34.19 KB 34.51 KB NODE_PROFILING
ReactFire-dev.js +0.7% +0.6% 850.37 KB 856.04 KB 189.83 KB 190.89 KB FB_WWW_DEV
ReactFire-prod.js 🔺+1.3% 🔺+1.0% 336.71 KB 340.97 KB 62.19 KB 62.83 KB FB_WWW_PROD
ReactFire-profiling.js +1.2% +1.1% 341.87 KB 345.92 KB 63.14 KB 63.82 KB FB_WWW_PROFILING
react-dom-test-utils.development.js +0.3% +0.1% 56.79 KB 56.93 KB 15.66 KB 15.68 KB UMD_DEV
react-dom-test-utils.production.min.js 🔺+0.8% 🔺+0.4% 10.67 KB 10.76 KB 3.93 KB 3.94 KB UMD_PROD
react-dom-test-utils.development.js +0.3% +0.1% 55.13 KB 55.27 KB 15.34 KB 15.36 KB NODE_DEV
react-dom-test-utils.production.min.js 🔺+0.8% 🔺+0.6% 10.43 KB 10.51 KB 3.86 KB 3.88 KB NODE_PROD
ReactTestUtils-dev.js +0.3% +0.1% 52.56 KB 52.7 KB 14.19 KB 14.21 KB FB_WWW_DEV
react-dom-unstable-native-dependencies.development.js 0.0% 0.0% 60.43 KB 60.43 KB 15.72 KB 15.72 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.0% 10.43 KB 10.43 KB 3.57 KB 3.57 KB NODE_PROD
react-dom-server.browser.development.js +0.1% +0.1% 137.07 KB 137.21 KB 36.15 KB 36.17 KB UMD_DEV
react-dom-server.browser.production.min.js 🔺+0.5% 🔺+0.2% 19.11 KB 19.2 KB 7.21 KB 7.23 KB UMD_PROD
react-dom-server.browser.development.js +0.1% +0.1% 133.2 KB 133.34 KB 35.21 KB 35.23 KB NODE_DEV
react-dom-server.browser.production.min.js 🔺+0.5% 🔺+0.3% 19.03 KB 19.12 KB 7.2 KB 7.22 KB NODE_PROD
ReactDOMServer-dev.js +0.1% +0.1% 135.39 KB 135.54 KB 34.78 KB 34.8 KB FB_WWW_DEV
ReactDOMServer-prod.js 🔺+0.3% 🔺+0.2% 47.85 KB 47.98 KB 11.01 KB 11.04 KB FB_WWW_PROD
react-dom-server.node.development.js +0.1% +0.1% 135.14 KB 135.28 KB 35.76 KB 35.78 KB NODE_DEV
react-dom-server.node.production.min.js 🔺+0.4% 🔺+0.2% 19.9 KB 19.98 KB 7.51 KB 7.53 KB NODE_PROD
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.1% 1.21 KB 1.21 KB 707 B 706 B UMD_PROD
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 3.88 KB 3.88 KB 1.51 KB 1.51 KB NODE_DEV

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.9% +0.8% 567.51 KB 572.63 KB 124.53 KB 125.49 KB UMD_DEV
react-art.production.min.js 🔺+0.9% 🔺+1.1% 95.9 KB 96.79 KB 29.47 KB 29.79 KB UMD_PROD
react-art.development.js +1.0% +0.9% 498.38 KB 503.5 KB 107.08 KB 108.03 KB NODE_DEV
react-art.production.min.js 🔺+1.5% 🔺+1.5% 60.88 KB 61.77 KB 18.75 KB 19.04 KB NODE_PROD
ReactART-dev.js +1.1% +1.0% 507.83 KB 513.49 KB 106.16 KB 107.23 KB FB_WWW_DEV
ReactART-prod.js 🔺+2.1% 🔺+2.0% 197.11 KB 201.22 KB 33.52 KB 34.18 KB FB_WWW_PROD

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-dev.js +0.9% +0.8% 640.13 KB 645.78 KB 136.49 KB 137.57 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+1.7% 🔺+1.6% 245.09 KB 249.19 KB 42.55 KB 43.25 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +1.5% +1.5% 253.05 KB 256.92 KB 44.2 KB 44.85 KB RN_FB_PROFILING
ReactNativeRenderer-dev.js +0.9% +0.8% 640.04 KB 645.7 KB 136.45 KB 137.54 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+1.7% 🔺+1.7% 245.1 KB 249.2 KB 42.55 KB 43.25 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +1.5% +1.5% 253.07 KB 256.94 KB 44.2 KB 44.85 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.9% +0.8% 628.88 KB 634.54 KB 133.73 KB 134.82 KB RN_FB_DEV
ReactFabric-prod.js 🔺+1.7% 🔺+1.7% 238.25 KB 242.38 KB 41.23 KB 41.92 KB RN_FB_PROD
ReactFabric-profiling.js +1.6% +1.5% 246.22 KB 250.13 KB 42.93 KB 43.56 KB RN_FB_PROFILING
ReactFabric-dev.js +0.9% +0.8% 628.79 KB 634.44 KB 133.7 KB 134.79 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+1.7% 🔺+1.7% 238.26 KB 242.39 KB 41.23 KB 41.92 KB RN_OSS_PROD
ReactFabric-profiling.js +1.6% +1.5% 246.23 KB 250.14 KB 42.93 KB 43.56 KB RN_OSS_PROFILING

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-test-renderer.development.js +1.0% +0.9% 512.44 KB 517.57 KB 109.95 KB 110.92 KB UMD_DEV
react-test-renderer.production.min.js 🔺+1.4% 🔺+1.7% 62.17 KB 63.06 KB 19.12 KB 19.44 KB UMD_PROD
react-test-renderer.development.js +1.0% +0.9% 507.98 KB 513.11 KB 108.85 KB 109.82 KB NODE_DEV
react-test-renderer.production.min.js 🔺+1.4% 🔺+1.4% 61.86 KB 62.75 KB 18.98 KB 19.24 KB NODE_PROD
ReactTestRenderer-dev.js +1.1% +1.0% 519.53 KB 525.18 KB 108.72 KB 109.81 KB FB_WWW_DEV
react-test-renderer-shallow.development.js +0.3% +0.2% 41.8 KB 41.95 KB 10.81 KB 10.83 KB UMD_DEV
react-test-renderer-shallow.production.min.js 🔺+0.8% 🔺+0.6% 11.51 KB 11.6 KB 3.53 KB 3.55 KB UMD_PROD
react-test-renderer-shallow.development.js +0.4% +0.3% 35.94 KB 36.08 KB 9.44 KB 9.46 KB NODE_DEV
react-test-renderer-shallow.production.min.js 🔺+0.7% 🔺+0.6% 11.66 KB 11.74 KB 3.65 KB 3.67 KB NODE_PROD
ReactShallowRenderer-dev.js +0.4% +0.3% 34.74 KB 34.88 KB 8.72 KB 8.75 KB FB_WWW_DEV

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +1.0% +0.9% 496.17 KB 501.3 KB 105.56 KB 106.51 KB NODE_DEV
react-reconciler.production.min.js 🔺+1.4% 🔺+1.6% 61.93 KB 62.77 KB 18.55 KB 18.86 KB NODE_PROD
react-reconciler-persistent.development.js +1.0% +0.9% 493.85 KB 498.97 KB 104.59 KB 105.54 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+1.4% 🔺+1.6% 61.94 KB 62.78 KB 18.56 KB 18.86 KB NODE_PROD
react-reconciler-reflection.development.js +0.8% +0.4% 19.01 KB 19.16 KB 6.06 KB 6.08 KB NODE_DEV
react-reconciler-reflection.production.min.js 🔺+3.6% 🔺+2.1% 2.43 KB 2.51 KB 1.09 KB 1.12 KB NODE_PROD

Generated by 🚫 dangerJS

@gaearon
Copy link
Collaborator

gaearon commented May 9, 2019

Why does this example show a "user blocking update suspended" if I click more than once?
https://gist.github.com/gaearon/a9db55f4341ebe31c9f59c9bfab63333

@sebmarkbage
Copy link
Collaborator Author

@gaearon You need scheduler.next too.

@gaearon
Copy link
Collaborator

gaearon commented May 9, 2019

OK this does work better. https://gist.github.com/gaearon/15f4b0f8265a1988428f7f60f347d80d

@@ -220,6 +220,7 @@ ReactBatch.prototype.render = function(children: ReactNodeList) {
internalRoot,
null,
expirationTime,
null,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that batches never suspend, but I'm not sure it makes sense for them to suspend anyway since they control the commit anyway. I'm not sure when the promise should resolve in this new multi-commit world.

// Within the scope of the callback, mark all updates as being allowed to suspend.
export function suspendIfNeeded(scope: () => void, config?: SuspenseConfig) {
const previousConfig = ReactCurrentBatchConfig.suspense;
ReactCurrentBatchConfig.suspense = config || DefaultSuspenseConfig;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Explicit comparison?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we do elsewhere? I don't want any bad types like undefined or false to leak into the implementation since it might not immediately throw only deopt, and I don't want to add a bunch of prod runtime code since this should be a fairly very light operation.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elsewhere we check for null and undefined explicitly, like

ReactCurrentBatchConfig.suspense = config !== undefined && config !== null ? config : DefaultSuspenseConfig;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I'm wondering why you've chosen a different pattern here, and whether we should go change the other sites. Like root.render:

ReactRoot.prototype.render = function(
children: ReactNodeList,
callback: ?() => mixed,
): Work {
const root = this._internalRoot;
const work = new ReactWork();
callback = callback === undefined ? null : callback;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I wonder if null should be resetting it to not suspend? I guess you can also pass in timeout: 0 but then that opts you out of JND.

const previousConfig = ReactCurrentBatchConfig.suspense;
ReactCurrentBatchConfig.suspense = config || DefaultSuspenseConfig;
try {
scope();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this return? For consistency with flushSync, next, runWithPriority, et al.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess. What is that ever used for? I could imagine things like async functions but that seems like very confusing bad practice to use an async function here.

export function markRenderEventTime(expirationTime: ExpirationTime): void {
if (expirationTime < workInProgressRootMostRecentEventTime) {
workInProgressRootMostRecentEventTime = expirationTime;
export function markRenderEventTimeAndConfig(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming suggestion: markBatchConfig

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

markExpirationAndSuspenseConfig?

Several callers must supply the expiration but don't supply a config, and that config likely wouldn't be the "whole batch's config" but specifically it is where you pass an object or null that is the interesting flag here.

// We don't know exactly when the update was scheduled, but we can infer an
// approximate start time from the expiration time.
const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
return (
earliestExpirationTimeMs -
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's store the start time on the batch config so we don't have to infer it? Can do in a follow up.

Copy link
Collaborator

@acdlite acdlite May 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However the event time computation is pretty broken right now and it might be nice to fix it soon so we can rule it out as a source of possible wonkiness when we get bug reports.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to have to create a new object for the batch too.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it not be the same object as suspenseConfig?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suspenseConfig is a user space object so mutating it gets fragile. I also expect it to mostly be a hoisted shared object. E.g. the default one right now but also the defaults in hooks would just use a hoisted config.

We could have another field for that but that requires me to track another thing for the render path. It's probably the right strategy eventually but I think we need to think about it some more.

@acdlite
Copy link
Collaborator

acdlite commented May 14, 2019

Is this ready for review? Don't see any tests

@sebmarkbage sebmarkbage changed the title Add suspendIfNeeded API Add withSuspenseConfig API May 16, 2019
@gaearon
Copy link
Collaborator

gaearon commented May 16, 2019

How do you decide what to put on React (this) vs Scheduler (next)? I understand React gives a stronger sintleton guarantee but as a user the distinction seems blurred to me. Especially because a bunch of similar modifiers live on the renderer (flushSync).

Adds a "current" suspense config that gets applied to all updates scheduled
during the current scope.

I suspect we might want to add other types of configurations to the "batch"
so I called it the "batch config".

This works across renderers/roots but they won't actually necessarily go
into the same batch.
We'll use this info to suspend a commit for longer when necessary.
This lets us track which renders we want to suspend for a short time vs
a longer time if possible.
This can be used to implement spinners that don't flicker if the data
and rendering is really fast.
This is a required argument in the type signature but people may not
supply it and this is a user facing object.
This allow opting out of suspending in some nested scope.

A lot of time when you use this function you'll use it with high level
helpers. Those helpers often want to accept some additional configuration
for suspense and if it should suspend at all. The easiest way is to just
have the api accept null or a suspense config and pass it through. However,
then you have to remember that calling suspendIfNeeded has a default.

It gets simpler by just saying tat you can pass the config. You can have
your own default in user space.
This was referenced Nov 4, 2024
This was referenced Nov 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants