facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
228.89k stars 46.83k forks source link

Bug: Suspense Update Yields Too Willingly in Concurrent Mode #31149

Open jzhan-canva opened 3 weeks ago

jzhan-canva commented 3 weeks ago

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.

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

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.

edqwerty1 commented 3 weeks ago

Sounds similar to the issues I have been having https://github.com/facebook/react/issues/31099

jzhan-canva commented 3 weeks ago

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