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

[Hydration Bugfix] Updates to dehydrated content when disableSchedulerTimeoutBasedOnReactExpirationTime is enabled #16614

Closed
wants to merge 2 commits into from

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Aug 30, 2019

The Bug

When server rendered content that hasn't finished hydrating yet ("dehydrated" content) receives an update (via props or context), React has a mechanism to force the content to hydrate before applying the update. It does this by increasing the priority of the hydration task from Idle to a level slightly higher than the current render. React will abort the current render, perform the hydration, then try the update again on top of the now-fully-hydrated content.

There are unit tests that cover this case. The bug starts happening when disableSchedulerTimeoutBasedOnReactExpirationTime is enabled. It turns out that the mechanism to interrupt the current rendering task depends on the hydration task having a slightly earlier timeout, because Scheduler tasks are sorted by their timeouts. When the hydration task is higher priority, it causes shouldYield to flip to true, forcing the render to yield execution and allowing the hydration task to start. (This is similar to how input events can interrupt normal priority renders.)

disableSchedulerTimeoutBasedOnReactExpirationTime breaks this mechanism, because when it is enabled, the timeout given to Scheduler is no longer based on React's internal expiration times. Effectively, all rendering tasks within the same priority category are first-in-first-out. So, the hydration task comes after the original task in the Scheduler queue, and therefore shouldYield will keep returning false, and the original task will run to completion. (See #16284 for more information on disableSchedulerTimeoutBasedOnReactExpirationTime.)

The first commit in this PR adds a regression test for this case.

The Fix

There are several potential fixes. The one I've chosen is not ideal in the long term, but it's lower risk compared to the complete solution, which will likely require some refactoring of how rendering tasks are scheduled.

The work loop already has some logic to cancel a rendering task in favor of a higher priority one, using Scheduler.cancelCallback. It does this by comparing the React expiration times of each task, so it doesn't depend on the ordering of tasks in Scheduler. This works when the high priority task is received during an input event.

However, Scheduler.cancelCallback is currently a no-op when given an already-running task. It does not cause the task to stop execution, and if the task does yield with a continuation, then the continuation will run. Which means it won't work if React is already inside the render phase. (Note the distinction between "inside the render phase" versus "in an event that fires in between two chunks of a time sliced task.")

The fix in the second commit addresses both parts: canceling the current task causes shouldYield to return true, and if the canceled task returns a continuation, the continuation is ignored.

This is sufficient to fix the regression.

Alternative Fixes

A proper fix would be to model interruptions of in-progress renders in such a way that it does not depend on Scheduler's semantics for canceling and yielding. However, because of the inherent risk involved in changing how rendering tasks are scheduled, I would prefer to land this smaller fix first before attempting a refactor.

(There's already a planned mini-refactor of the work loop, e.g. to optimize how pings and restarts are modeled. We can fold this into that larger change.)

Adds a test case for receving an update to dehydrated content when the
`disableSchedulerTimeoutBasedOnReactExpirationTime` flag is enabled.
Canceling an already running task is currently a no-op. It does not
cause the task to yield execution, and if the task does yield with
a continuation, then the continuation will run.

This change addresses both parts: canceling the current task causes
`shouldYield` to return `true`, and if the canceled task returns a
continuation, the continuation is ignored.

This fixes the regression test introduced in the preceding commit.
There's likely a better way to model a restart of an in-progress
render, but this approach is conceptually similar to how a regular
high priority interruption already works.
@sizebot
Copy link

sizebot commented Aug 30, 2019

Details of bundled changes.

Comparing: 4ef2696...83102b6

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.1% +0.2% 650.62 KB 651.4 KB 142.02 KB 142.3 KB UMD_DEV
react-art.production.min.js 0.0% -0.0% 101.86 KB 101.86 KB 31.14 KB 31.14 KB UMD_PROD
react-art.development.js +0.1% +0.2% 581.49 KB 582.27 KB 124.62 KB 124.91 KB NODE_DEV
react-art.production.min.js 0.0% -0.0% 66.87 KB 66.87 KB 20.39 KB 20.38 KB NODE_PROD
ReactART-dev.js +0.1% +0.2% 596.5 KB 597.29 KB 124.34 KB 124.63 KB FB_WWW_DEV

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.profiling.min.js 0.0% -0.0% 115.25 KB 115.25 KB 36.34 KB 36.33 KB NODE_PROFILING
ReactDOM-dev.js +0.1% +0.1% 933.83 KB 934.62 KB 206.71 KB 206.99 KB FB_WWW_DEV
react-dom-unstable-fizz.browser.development.js 0.0% +0.2% 3.78 KB 3.78 KB 1.53 KB 1.53 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 11.18 KB 11.18 KB 4.15 KB 4.15 KB UMD_PROD
react-dom-unstable-fizz.browser.development.js 0.0% +0.1% 3.61 KB 3.61 KB 1.48 KB 1.48 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.1% 10.95 KB 10.95 KB 4.09 KB 4.09 KB NODE_PROD
react-dom.development.js +0.1% +0.1% 909.72 KB 910.5 KB 206.59 KB 206.89 KB UMD_DEV
react-dom.development.js +0.1% +0.1% 904.01 KB 904.79 KB 204.98 KB 205.28 KB NODE_DEV
ReactDOM-prod.js 0.0% -0.0% 369.71 KB 369.71 KB 67.8 KB 67.8 KB FB_WWW_PROD
react-dom-server.browser.production.min.js 0.0% -0.0% 19.66 KB 19.66 KB 7.33 KB 7.33 KB NODE_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.71 KB 60.71 KB 15.85 KB 15.85 KB UMD_DEV
ReactDOMServer-prod.js 0.0% 0.0% 48.13 KB 48.13 KB 11.05 KB 11.05 KB FB_WWW_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.39 KB 60.39 KB 15.72 KB 15.72 KB NODE_DEV

react-test-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactTestRenderer-dev.js +0.1% +0.2% 607.58 KB 608.36 KB 126.76 KB 127.04 KB FB_WWW_DEV
react-test-renderer-shallow.development.js 0.0% -0.0% 33.18 KB 33.18 KB 8.49 KB 8.49 KB NODE_DEV
react-test-renderer.development.js +0.1% +0.2% 594.65 KB 595.43 KB 127.33 KB 127.63 KB UMD_DEV
react-test-renderer.production.min.js 0.0% -0.0% 68.81 KB 68.81 KB 21.16 KB 21.16 KB UMD_PROD
react-test-renderer.development.js +0.1% +0.2% 590.19 KB 590.96 KB 126.19 KB 126.48 KB NODE_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% 580.13 KB 580.91 KB 123.29 KB 123.59 KB NODE_DEV
react-reconciler.production.min.js 0.0% -0.0% 68.87 KB 68.87 KB 20.43 KB 20.42 KB NODE_PROD
react-reconciler-reflection.production.min.js 0.0% -0.2% 2.56 KB 2.56 KB 1.14 KB 1.14 KB NODE_PROD
react-reconciler-persistent.development.js +0.1% +0.2% 577.17 KB 577.95 KB 122.05 KB 122.34 KB NODE_DEV

react-native-renderer

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
ReactNativeRenderer-profiling.js 0.0% -0.0% 278.25 KB 278.25 KB 47.78 KB 47.78 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.1% +0.2% 735.71 KB 736.5 KB 155.84 KB 156.12 KB RN_FB_DEV
ReactNativeRenderer-dev.js +0.1% +0.2% 729.16 KB 729.95 KB 154.68 KB 154.96 KB RN_OSS_DEV
ReactNativeRenderer-dev.js +0.1% +0.2% 729.32 KB 730.11 KB 154.76 KB 155.04 KB RN_FB_DEV
ReactNativeRenderer-profiling.js 0.0% -0.0% 278.24 KB 278.24 KB 47.79 KB 47.79 KB RN_FB_PROFILING
ReactFabric-dev.js +0.1% +0.2% 735.54 KB 736.33 KB 155.77 KB 156.05 KB RN_OSS_DEV

Generated by 🚫 dangerJS

ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableSuspenseServerRenderer = true;
ReactFeatureFlags.enableSuspenseCallback = true;
ReactFeatureFlags.enableFlareAPI = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this intentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Probably not necessary, I just copy pasted from the top of the file for consistency:

jest.resetModuleRegistry();
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableSuspenseServerRenderer = true;
ReactFeatureFlags.enableSuspenseCallback = true;
ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
act = require('react-dom/test-utils').act;
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
Suspense = React.Suspense;
SuspenseList = React.unstable_SuspenseList;

@@ -288,6 +288,26 @@ describe('Scheduler', () => {
expect(Scheduler).toFlushWithoutYielding();
});

it('cancelling the currently running task', () => {
const task = scheduleCallback(NormalPriority, () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there any other priorities we need to test cancelling on other than normal? Should there be a test to confirm that discrete priority events do not get cancelled?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The priority shouldn't be relevant, but I'll add the same test at user-blocking priority as an extra precaution.

Copy link
Contributor

@trueadm trueadm left a comment

Choose a reason for hiding this comment

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

I understand the implementation here and understand what you're doing here and it looks fine for a short-term solution, as I agree – depending on the scheduler's semantics for canceling and yielding is a bit leaky.

I also added some comments in regards to some things though that you might want to respond to.

@necolas necolas added the React Core Team Opened by a member of the React Core Team label Jan 8, 2020
@acdlite
Copy link
Collaborator Author

acdlite commented Jan 23, 2020

Superseded by #16771

@acdlite acdlite closed this Jan 23, 2020
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.

5 participants