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

Memoize promise listeners to prevent exponential growth #14429

Merged
merged 3 commits into from
Dec 14, 2018

Conversation

acdlite
Copy link
Collaborator

@acdlite acdlite commented Dec 13, 2018

Previously, React would attach a new listener every time a promise is thrown, regardless of whether the same listener was already attached during a previous render. Because React attempts to render every time a promise resolves, the number of listeners grows quickly.

This was especially bad in synchronous mode because the renders that happen when the promise pings are not batched together. So if a single promise has multiple listeners for the same root, there will be multiple renders, which in turn results in more listeners being added to the remaining unresolved promises. This results in exponential growth in the number of listeners with respect to the number of IO-bound components in a single render.

Fixes #14220

@sizebot
Copy link

sizebot commented Dec 13, 2018

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

Details of bundled changes.

Comparing: 535804f...3b60eef

react-dom

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-dom.development.js +0.3% +0.2% 724.65 KB 726.61 KB 167.54 KB 167.94 KB UMD_DEV
react-dom.production.min.js 🔺+0.7% 🔺+0.8% 97.81 KB 98.48 KB 31.85 KB 32.1 KB UMD_PROD
react-dom.profiling.min.js +0.7% +0.6% 100.8 KB 101.46 KB 32.56 KB 32.77 KB UMD_PROFILING
react-dom.development.js +0.3% +0.2% 719.7 KB 721.67 KB 166.14 KB 166.52 KB NODE_DEV
react-dom.production.min.js 🔺+0.7% 🔺+0.6% 97.81 KB 98.47 KB 31.39 KB 31.58 KB NODE_PROD
react-dom.profiling.min.js +0.7% +0.6% 100.89 KB 101.58 KB 32 KB 32.18 KB NODE_PROFILING
ReactDOM-dev.js +0.3% +0.2% 741.1 KB 743 KB 167.13 KB 167.53 KB FB_WWW_DEV
ReactDOM-prod.js 🔺+0.3% 🔺+0.6% 308.21 KB 309.08 KB 56.79 KB 57.13 KB FB_WWW_PROD
ReactDOM-profiling.js +0.4% +0.5% 315.1 KB 316.28 KB 58.22 KB 58.49 KB FB_WWW_PROFILING
react-dom-unstable-fire.development.js +0.3% +0.2% 724.93 KB 726.9 KB 167.65 KB 168.05 KB UMD_DEV
react-dom-unstable-fire.production.min.js 🔺+0.7% 🔺+0.8% 97.83 KB 98.49 KB 31.86 KB 32.1 KB UMD_PROD
react-dom-unstable-fire.profiling.min.js +0.7% +0.6% 100.81 KB 101.47 KB 32.57 KB 32.77 KB UMD_PROFILING
react-dom-unstable-fire.development.js +0.3% +0.2% 719.98 KB 721.96 KB 166.25 KB 166.64 KB NODE_DEV
react-dom-unstable-fire.production.min.js 🔺+0.7% 🔺+0.6% 97.82 KB 98.49 KB 31.4 KB 31.59 KB NODE_PROD
react-dom-unstable-fire.profiling.min.js +0.7% +0.6% 100.91 KB 101.59 KB 32.01 KB 32.19 KB NODE_PROFILING
ReactFire-dev.js +0.3% +0.2% 740.25 KB 742.15 KB 167.06 KB 167.46 KB FB_WWW_DEV
ReactFire-prod.js 🔺+0.3% 🔺+0.8% 296.8 KB 297.67 KB 54.36 KB 54.79 KB FB_WWW_PROD
ReactFire-profiling.js +0.4% +0.5% 303.62 KB 304.8 KB 55.84 KB 56.13 KB FB_WWW_PROFILING
react-dom-test-utils.development.js 0.0% -0.0% 44.87 KB 44.87 KB 12.3 KB 12.3 KB UMD_DEV
react-dom-test-utils.production.min.js 0.0% -0.1% 9.97 KB 9.97 KB 3.71 KB 3.71 KB UMD_PROD
react-dom-test-utils.development.js 0.0% -0.0% 44.59 KB 44.59 KB 12.24 KB 12.24 KB NODE_DEV
react-dom-test-utils.production.min.js 0.0% -0.0% 9.74 KB 9.74 KB 3.65 KB 3.65 KB NODE_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.61 KB 60.61 KB 15.92 KB 15.92 KB UMD_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 11.01 KB 11.01 KB 3.81 KB 3.81 KB UMD_PROD
react-dom-unstable-native-dependencies.development.js 0.0% -0.0% 60.29 KB 60.29 KB 15.79 KB 15.79 KB NODE_DEV
react-dom-unstable-native-dependencies.production.min.js 0.0% -0.1% 10.75 KB 10.75 KB 3.71 KB 3.71 KB NODE_PROD
react-dom-server.browser.development.js 0.0% -0.0% 123.89 KB 123.89 KB 33.09 KB 33.09 KB UMD_DEV
react-dom-server.browser.production.min.js 0.0% -0.0% 16.99 KB 16.99 KB 6.53 KB 6.52 KB UMD_PROD
react-dom-server.browser.development.js 0.0% -0.0% 120.02 KB 120.02 KB 32.16 KB 32.16 KB NODE_DEV
react-dom-server.browser.production.min.js 0.0% -0.0% 16.9 KB 16.9 KB 6.52 KB 6.52 KB NODE_PROD
ReactDOMServer-dev.js 0.0% -0.0% 121.2 KB 121.2 KB 31.79 KB 31.79 KB FB_WWW_DEV
ReactDOMServer-prod.js 0.0% -0.0% 44.42 KB 44.42 KB 10.3 KB 10.3 KB FB_WWW_PROD
react-dom-server.node.development.js 0.0% -0.0% 122.08 KB 122.08 KB 32.71 KB 32.7 KB NODE_DEV
react-dom-server.node.production.min.js 0.0% -0.0% 17.77 KB 17.77 KB 6.83 KB 6.83 KB NODE_PROD
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.63 KB 3.63 KB 1.44 KB 1.44 KB UMD_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.3% 1.21 KB 1.21 KB 706 B 704 B UMD_PROD
react-dom-unstable-fizz.browser.development.js 0.0% -0.1% 3.45 KB 3.45 KB 1.39 KB 1.39 KB NODE_DEV
react-dom-unstable-fizz.browser.production.min.js 0.0% -0.3% 1.05 KB 1.05 KB 637 B 635 B NODE_PROD
react-dom-unstable-fizz.node.development.js 0.0% -0.1% 3.7 KB 3.7 KB 1.42 KB 1.42 KB NODE_DEV
react-dom-unstable-fizz.node.production.min.js 0.0% -0.4% 1.1 KB 1.1 KB 668 B 665 B NODE_PROD

react-art

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-art.development.js +0.4% +0.4% 505.05 KB 507.02 KB 111.4 KB 111.8 KB UMD_DEV
react-art.production.min.js 🔺+0.7% 🔺+0.7% 89.98 KB 90.59 KB 27.67 KB 27.85 KB UMD_PROD
react-art.development.js +0.5% +0.4% 436.54 KB 438.52 KB 94.28 KB 94.67 KB NODE_DEV
react-art.production.min.js 🔺+1.1% 🔺+0.8% 54.95 KB 55.57 KB 17 KB 17.14 KB NODE_PROD
ReactART-dev.js +0.4% +0.4% 444.78 KB 446.68 KB 93.25 KB 93.64 KB FB_WWW_DEV
ReactART-prod.js 🔺+0.8% 🔺+0.9% 183.65 KB 185.16 KB 31.4 KB 31.7 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.3% +0.3% 569.25 KB 571.15 KB 123.75 KB 124.15 KB RN_FB_DEV
ReactNativeRenderer-prod.js 🔺+0.6% 🔺+0.9% 239.25 KB 240.71 KB 41.98 KB 42.35 KB RN_FB_PROD
ReactNativeRenderer-profiling.js +0.7% +0.9% 245.12 KB 246.89 KB 43.35 KB 43.75 KB RN_FB_PROFILING
ReactNativeRenderer-dev.js +0.3% +0.3% 569.17 KB 571.06 KB 123.72 KB 124.11 KB RN_OSS_DEV
ReactNativeRenderer-prod.js 🔺+0.7% 🔺+0.9% 224.38 KB 225.84 KB 38.97 KB 39.33 KB RN_OSS_PROD
ReactNativeRenderer-profiling.js +0.8% +1.0% 230.12 KB 231.89 KB 40.31 KB 40.72 KB RN_OSS_PROFILING
ReactFabric-dev.js +0.3% +0.3% 560.1 KB 562 KB 121.45 KB 121.85 KB RN_FB_DEV
ReactFabric-prod.js 🔺+0.2% 🔺+0.3% 233.4 KB 233.92 KB 40.8 KB 40.9 KB RN_FB_PROD
ReactFabric-profiling.js +0.4% +0.3% 238.36 KB 239.24 KB 42.16 KB 42.26 KB RN_FB_PROFILING
ReactFabric-dev.js +0.3% +0.3% 560.01 KB 561.91 KB 121.4 KB 121.8 KB RN_OSS_DEV
ReactFabric-prod.js 🔺+0.2% 🔺+0.3% 218.55 KB 219.07 KB 37.72 KB 37.84 KB RN_OSS_PROD
ReactFabric-profiling.js +0.4% +0.3% 223.31 KB 224.19 KB 39.13 KB 39.25 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 +0.4% +0.4% 449.77 KB 451.73 KB 97.06 KB 97.44 KB UMD_DEV
react-test-renderer.production.min.js 🔺+1.2% 🔺+1.5% 56.28 KB 56.97 KB 17.27 KB 17.53 KB UMD_PROD
react-test-renderer.development.js +0.4% +0.4% 444.72 KB 446.69 KB 95.84 KB 96.22 KB NODE_DEV
react-test-renderer.production.min.js 🔺+1.2% 🔺+1.4% 55.94 KB 56.64 KB 17.13 KB 17.37 KB NODE_PROD
ReactTestRenderer-dev.js +0.4% +0.4% 453.15 KB 455.04 KB 95.12 KB 95.52 KB FB_WWW_DEV
react-test-renderer-shallow.development.js 0.0% -0.0% 25.88 KB 25.88 KB 7.06 KB 7.05 KB UMD_DEV
react-test-renderer-shallow.production.min.js 0.0% -0.0% 7.31 KB 7.31 KB 2.39 KB 2.39 KB UMD_PROD
react-test-renderer-shallow.development.js 0.0% -0.0% 20.18 KB 20.18 KB 5.61 KB 5.61 KB NODE_DEV
react-test-renderer-shallow.production.min.js 0.0% -0.1% 7.96 KB 7.96 KB 2.64 KB 2.64 KB NODE_PROD

react-reconciler

File Filesize Diff Gzip Diff Prev Size Current Size Prev Gzip Current Gzip ENV
react-reconciler.development.js +0.5% +0.4% 434.37 KB 436.35 KB 92.79 KB 93.2 KB NODE_DEV
react-reconciler.production.min.js 🔺+1.1% 🔺+0.9% 56.07 KB 56.71 KB 16.83 KB 16.99 KB NODE_PROD
react-reconciler-persistent.development.js +0.5% +0.5% 432.75 KB 434.73 KB 92.14 KB 92.55 KB NODE_DEV
react-reconciler-persistent.production.min.js 🔺+1.1% 🔺+0.9% 56.08 KB 56.72 KB 16.84 KB 16.99 KB NODE_PROD
react-reconciler-reflection.development.js 0.0% -0.0% 15.4 KB 15.4 KB 4.83 KB 4.83 KB NODE_DEV
react-reconciler-reflection.production.min.js 0.0% -0.1% 2.56 KB 2.56 KB 1.13 KB 1.13 KB NODE_PROD

Generated by 🚫 dangerJS

Copy link
Contributor

@threepointone threepointone left a comment

Choose a reason for hiding this comment

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

a learning review for me.

@acdlite acdlite force-pushed the memoize-promise-listeners branch 2 times, most recently from 891fcf2 to 79eb26e Compare December 14, 2018 03:35
);
// runPlaceholderTests('ReactSuspensePlaceholder (mutation)', () =>
// require('react-noop-renderer'),
// );
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why disable these?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Commented it out while debugging, meant to add back. Although now I just noticed... I screwed up when I converted this to use the test renderer. It's not actually testing anything in concurrent mode anymore 🤦

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'll fix that in a separate PR since it's unrelated


// If this boundary just timed out, then it will have a set of thenables.
// For each thenable, attach a listener so that when it resolves, React
// attempts to re-render the boundary in the primary (pre-timeout) state.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought we wanted to do this work in the passive effect? Is there a reason not to?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Only that it seemed like a riskier change. I'll add a follow-up.

@sebmarkbage
Copy link
Collaborator

You still need to add the lint config to a few more places like RN.

Previously, React would attach a new listener every time a promise is
thrown, regardless of whether the same listener was already attached
during a previous render. Because React attempts to render every time
a promise resolves, the number of listeners grows quickly.

This was especially bad in synchronous mode because the renders that
happen when the promise pings are not batched together. So if a single
promise has multiple listeners for the same root, there will be multiple
renders, which in turn results in more listeners being added to the
remaining unresolved promises. This results in exponential growth in
the number of listeners with respect to the number of IO-bound
components in a single render.

Fixes facebook#14220
@acdlite acdlite merged commit 4a10721 into facebook:master Dec 14, 2018
@Jessidhia
Copy link
Contributor

"Because React attempts to render every time a promise resolves" ah, so it's intentional. I thought it was a bug when I encountered this in test code (I thought React would wait for all thrown Promises to resolve before attempting to rerender a Suspense. Won't this just cause another suspend again?)

@acdlite
Copy link
Collaborator Author

acdlite commented Dec 23, 2018

@Kovensky Every time a promise pings it means some part of the tree has been unblocked, so it’s worth rendering again, even before all the promises resolve, to fire the nested requests.

@gaearon
Copy link
Collaborator

gaearon commented Jan 14, 2019

Is there any chance that "optional" dependency on WeakMap and WeakSet somehow changes behavior when they're not available? There's been two reported issues with Suspense behaving weirdly on IE11 since this PR.

#14570
#14583

@threepointone
Copy link
Contributor

^ Just dropping a note here that the above issues were because of a bad polyfill, and have been 'resolved'. This PR is fine.

jetoneza pushed a commit to jetoneza/react that referenced this pull request Jan 23, 2019
* Memoize promise listeners to prevent exponential growth

Previously, React would attach a new listener every time a promise is
thrown, regardless of whether the same listener was already attached
during a previous render. Because React attempts to render every time
a promise resolves, the number of listeners grows quickly.

This was especially bad in synchronous mode because the renders that
happen when the promise pings are not batched together. So if a single
promise has multiple listeners for the same root, there will be multiple
renders, which in turn results in more listeners being added to the
remaining unresolved promises. This results in exponential growth in
the number of listeners with respect to the number of IO-bound
components in a single render.

Fixes facebook#14220

* Memoize on the root and Suspense fiber instead of on the promise

* Add TODO to fix persistent mode tests
n8schloss pushed a commit to n8schloss/react that referenced this pull request Jan 31, 2019
* Memoize promise listeners to prevent exponential growth

Previously, React would attach a new listener every time a promise is
thrown, regardless of whether the same listener was already attached
during a previous render. Because React attempts to render every time
a promise resolves, the number of listeners grows quickly.

This was especially bad in synchronous mode because the renders that
happen when the promise pings are not batched together. So if a single
promise has multiple listeners for the same root, there will be multiple
renders, which in turn results in more listeners being added to the
remaining unresolved promises. This results in exponential growth in
the number of listeners with respect to the number of IO-bound
components in a single render.

Fixes facebook#14220

* Memoize on the root and Suspense fiber instead of on the promise

* Add TODO to fix persistent mode tests
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.

When using React.lazy will cause the GPU/CPU to run overloaded, and the page is very slow.
7 participants