-
Notifications
You must be signed in to change notific 8000 ation settings - Fork 48.4k
[React 19] Suspense throttling behavior (FALLBACK_THROTTLE_MS
) kicks in too often
#31819
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
Comments
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 |
@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 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. |
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. |
It appears that any time a lazy component is initialised it also kicks in the 300ms suspense, even if there is nothing that needs to load. We have our bundle split so we may have 10 or so lazy wrapped component (views) in one chunk, so the first time you hit one of the views the chunk is loaded, then switching between the views is instant. Now it seems that you have to wait 300ms and show a spinner even though nothing is being loaded after the initial chunk load, which is a notable degradation from an instant page change. After the lazy components initialise the changes are instant though on subsequent changes. But the artificial pause really isn't great. |
I'm fine with defaults, as long as there are escape hatches. This should 100% be configurable, with the option to disable it entirely. It's not really React's responsibility to make this kind of choice for every app. |
Seeing similar behavior, but only under webpack. Reproductions in mui/material-ui#44920 (comment). |
So I can remove the 'always 300ms' fallback by using However, mixing the use of deferred values/transitions with any non deferred/transition code can easily cause bugs. It's also sadly very easy to do. You need to be aware of what the internals of some hook or util may be doing. So the current situation seems to be:
I'll take a bit of flicker sometimes to avoid either of these. So configurability would be greatly appreciated. |
A Reddit discussion about this https://www.reddit.com/r/reactjs/comments/1hjoplz/react_19_scheduler_does_something_silly/ |
please add config option here. 300ms sucks. |
I'm also encountering this issue when using lazy routes on tanstack router and it's not clear to me how I could use This behaviour should really be opt-in not opt-out. |
300ms is still a significant amount of time. Many people have tried to pinpoint this exact problem, as their web applications significantly slowed down in terms of UX after migrating from React 18 to 19. |
This comment has been minimized.
This comment has been minimized.
It happens when triggering suspense boundaries. Was yours doing that but just resolving super fast so no one noticed? The canonical solution for now is to put the state change in a transition and it'll appear as before. But watch out if you're using useSyncExternalStore as the transitions don't work with it... which is kind of exacerbated by this change tbh 😅. |
This is really really really frustrating, my "loaders" are skeleton components that perfectly match the dimensions of my content, eg squares that should have photos. "flashing" the skeleton for 1ms and then fading into the loaded photo is totally fine and not at all jank. Now with react 19, even if all of my content is immediately available in a local cache, i still need to load all my content for 300ms?! why?! |
Then make it configurable. This isn't a decision the React gods should have over every React app. |
I put together a hackish Basically, I am storing the resolved import in the SWR cache, with the stringified importer function (with toString) serving as the cache key. This can also be adapted for usage with other caching solutions than I noticed that even if the import path used by Does anyone see any issues with it? It seems to work fine. I tested in dev/prod build, different browsers, local/deployed. I only tested Vite as a bundler. Maybe we could have a import useSWRImmutable from 'swr/immutable';
import { mutate } from 'swr';
import { useErrorBoundary } from 'react-error-boundary';
type ImportComponent<TProps extends object> = () => Promise<{
default: React.ComponentType<TProps>;
}>;
const getSWRKey = <TProps extends object>(
importComponent: ImportComponent<TProps>,
) => importComponent.toString();
/**
* Paired with `lazyWithCache`, this solves the issue where React briefly shows a Suspense fallback for a component loaded with React.lazy() even when the component had already been imported before it was rendered.
*
* Usage:
*
* `preloadComponent(() => import('path/to/component'));`
*/
export const preloadComponent = <TProps extends object>(
importComponent: ImportComponent<TProps>,
) => {
void mutate(getSWRKey(importComponent), importComponent);
};
/**
* Usage is similar to `React.lazy`:
*
* `const LazyLoadedComponent = lazyWithCache(() => import('./path/to/LazyLoadedComponent'));`
*/
export const lazyWithCache = <TProps extends object>(
importComponent: ImportComponent<TProps>,
) =>
function Lazy(props: TProps) {
const { showBoundary } = useErrorBoundary();
const {
data: { default: Component },
} = useSWRImmutable(getSWRKey(importComponent), importComponent, {
suspense: true,
onError: showBoundary,
});
return <Component {...props} />;
}; |
I also ended up reimplementing lazy without suspense and removing any other suspense boundaries. It was fairly simple and resolved the problems with this unfortunate decision. I just hope I can continue to avoid having to use these 'async' features. |
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
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.
The text was updated successfully, but these errors were encountered: