Bug: Suspense Update Yields Too Willingly in Concurrent Mode
Summary
React Version: 18.2.0 (also tested in 19 RC)
After transitioning from the legacy renderer to the concurrent renderer, we've encountered an issue with render delays when using React.lazy to lazily load a sub-application. This sub-app is a crucial component of our UI, and without it, the page is essentially non-functional.
- Legacy Renderer: Everying works as expected with no issues.
- Concurrent Renderer: Lazy loading works, but there's a significant delay in render time.
Business Context
As illustrated in the diagram below, the sub-app occupies the majority of the page. Currently, server-side rendering is not implemented for this page, so it relies on client-side rendering to determine which sub-app should be lazy-loaded.
The sub-app is the most crucial component of this page. Therefore, once the import() resolves, we'd like to prioritize rendering this component over any other task.
Given that this page is a large-scale product involving many engineers, transitioning to SSR is a challenging task.
+----------------------------------------------------+
| Top Nav Bar |
+----------------------------------------------------+
| | |
| | |
| L | |
| e | |
| f | |
| t | Sub-app |
| | <Suspense /> |
| | |
| B | |
| a | |
| r | |
| | |
| | |
+----+-----------------------------------------------+
Steps To Reproduce
import React from "react";
import { render, screen } from "@testing-library/react";
import { ExpensiveComponent } from "./App";
let resolve: (value?: unknown) => void;
const promise = new Promise((res) => {
resolve = res;
});
const LazyComponent = React.lazy(async () => {
await promise;
return { default: ExpensiveComponent }; // ExpensiveComponent is very slow to render
});
test("Suspense", async () => {
render(
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</React.Suspense>
);
// expect fallback rendered here
expect(screen.getByText(/Loading/i)).toBeInTheDocument();
// resolve lazy component
resolve?.();
// wait for current event loop to clear
await new Promise((res) => setTimeout(res, 0));
// In legacy renderer, ExpensiveComponent is fully rendered and committed here
// In concurrent renderer, ExpensiveComponent is not fully rendered yet
});
Investigation Findings
- In concurrent mode, when the lazy component is resolved, the Suspense boundary attempts to update in a retry lane using
requestRetryLane. - In concurrent mode, this retry lane is assigned the lowest priority, causing React to yield too willingly and resulting in render delays.
- The use of
flushSyncto prioritize rendering does not affectrequestRetryLane, preventing us from prioritizing the lazy component for mounting inSyncLane.
Question/Request
Could we have an option to prioritize specific React.lazy or Suspense components? Prioritizing our sub-app component is crucial because of its essential role in our page's functionality and performance needs.
We look forward to any insights, workarounds, or plans for this kind of prioritization feature in React.
Thank you for your continued efforts in improving React and for your support.
Sounds similar to the issues I have been having https://github.com/facebook/react/issues/31099
Hi @edqwerty1 yes it's very similar, but my case is client side rendering without SSR/hydration. and I saw someone suggested unstable_scheduleHydration in your issue as it can help prioritise a suspense boundary during hydration. Unfortunately I didn't find anything for rendering so I decided to create a new issue
This issue has been automatically marked as stale. If this issue is still affecting you, please leave any comment (for example, "bump"), and we'll keep it open. We are sorry that we haven't been able to prioritize it yet. If you have any new additional information, please include it with your comment!
Closing this issue after a prolonged period of inactivity. If this issue is still present in the latest release, please create a new issue with up-to-date information. Thank you!