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

[React 19] Suspense throttling behavior (FALLBACK_THROTTLE_MS) kicks in too often #31819

Open
uhyo opened this issue Dec 17, 2024 · 3 comments
Open

Comments

@uhyo
Copy link

uhyo commented Dec 17, 2024

React version: 19.0.0

Related Issues: #30408, #31697

This may be the intended behavior, but I'm opening this issue anyway because enough discussion hasn't been done in the related issues IMO and the current behavior still feels poor to me in that it makes it too easy to slow down actual user experience.

Steps To Reproduce

Code
const zero = Promise.resolve(0);

function getNumber(num) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(num), 100);
  });
}

function App() {
  const [count, setCount] = useState(zero);

  const countValue = use(count);

  useEffect(() => {
    console.log("countValue =", countValue, Date.now());
  }, [countValue]);

  return (
    <button
      onClick={() => {
        console.log("click", Date.now());
        setCount(getNumber(countValue + 1));
      }}
    >
      count is {countValue}
    </button>
  );
}

In short, when a rerendering suspends, you always have to wait for 300ms even if underlying data fetching has finished sooner.

In the attached example, when user pushes the button, a new Promise is passed to use(), which triggers Suspense. Even though that Promise resolves exactly after 100ms, the UI is updated only after 300ms.

I experienced this issue when using Jotai, a Suspense-based state management library.

Given that the throttling behavior kicks in even in this simplest situation, it seems impossible to implement a user experience that involves Suspension and is quicker than 300ms regardless of, say, user's network speed.

Link to code example:

https://codesandbox.io/p/sandbox/4f4r94

The current behavior

Almost always need to wait for 300ms.

The expected behavior

Maybe some better heuristic for enabling the throttling behavior? It would also be very nice to make this configurable.

@uhyo uhyo added the Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug label Dec 17, 2024
@eps1lon
Copy link
Collaborator

eps1lon commented Dec 18, 2024

The alternative of quickly flashing the Suspense boundary fallback isn't better from our experience. In real apps, that would usually mean unmounting a large chunk of the screen for a very short period which doesn't make for a pleasant UX.

There isn't a correct number here since this is just a heuristic. 300ms felt like a good middleground between avoiding jank and feeling too sluggish.

A real-world example would help illustrate the issue.

Keep in mind, that you can always wrap the update in startTransition and display smaller fallback while isPending from useTransition is true.

@eps1lon eps1lon added React 19 and removed Status: Unconfirmed A potential issue that we haven't yet confirmed as a bug labels Dec 18, 2024
@uhyo
Copy link
Author

uhyo commented Dec 18, 2024

@eps1lon Thank you for the response. I have two questions now:

First, I understand that flashing UI isn't good user experience, but I still don't see any reason to make user wait for extra hundreds milliseconds, especially when the situation is this simple where there is only one ongoing suspension.

300ms at maximum isn't always a reasonable cost for making the UI look a bit less janky IMO.

Secondly, I see that useTransition could work. However if I understand correctly transitions are for non-blocking updates; in other words, state updates are marked as non-urgent if performed within a transition.

Using transitions for letting user see new data more quickly is quite counterintuitive to me. Am I getting anything wrong?

I don't have a truly real-world example but I think I can prepare something that looks more real-worldy if wanted.

@Tasin5541
Copy link

There are use cases where the suspense bound components are very small and can take less than 100ms to load (depends on the server as well). We used to not show anything as a fallback to avoid the jankiness. This is intentional, so that initial bundle size is low but end user also doesn't have to see these fallbacks for every little lazy loaded components. Now with this 300ms hold up, there's no other way than showing a fallback, which in turn feels like a worse UX. Instead of making this behavior the default, it should be opt in based.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants