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

UI freezes when chrome devtools is open #2378

Open
jakobthomasson opened this issue May 23, 2023 · 4 comments
Open

UI freezes when chrome devtools is open #2378

jakobthomasson opened this issue May 23, 2023 · 4 comments
Labels

Comments

@jakobthomasson
Copy link

Hi!

User since several years and currently facing an issue which baffels me.
I'm not entirely sure that the behaviour is due to redux-saga, but I'm hoping you could provide me some context which will make me understand what is going on.

Scenario
A modal is open with the possibility to select dates which will be used to fetch data through a saga.
When pressing "save" in that modal the following flow is defined:

<button 
    onClick={() => {
      onClose(); // Updates parent state which closes the modal
      startSaveSelectedDates(dates); // start saga which will fetch selectedDate and update global state accordingly. 
    } 
/>

The onClose(); affects the state as it should but the render for the state update is not happening until the saga resolves.

Here are some details regarding the circumnstances of the issue:

  • When Chrome devtools is closed, issue dissapears.
  • My saga locks my UI written in React only when Chrome devtools is open.
  • If I open an Incognito tab without plugins the saga does not seem to lock the UI.
  • If I open an Incognito tab with plugins, the lock time gradually increases with the number of plugins active. All plugins seem to affect the lock time, some more than other.
  • If I add yield delay(1) in the beginnig of the saga, the issue dissapears.

Saga code

function* startSelectDatesSaga(
  action: ActionType<typeof flexHoursActions.startSelectDates>,
) { 
  // If a  "yield delay(1);" is added here the UI is not locked.  

  const { selectedDates } = action.payload;

  // This is a pure redux action, only updates the state selectedDates
  // Alot of heavy renders are triggered by this, the UI seems to lock until these renders are resolved
  yield put(flexHoursActions.setSelectedDates(selectedDates));

  // API call.
  // If this is moved above "yield put(flexHoursActions.setSelectedDates(selectedDates));" The UI does not lock. 
  const {
    employeeHours,
  }: SagaReturnType<typeof actualAPI.getEmployeeHours> = yield call(
    actualAPI.getEmployeeHours,
    {   
      dates: selectedDates,
    },
  );

  yield put(employeeHourActions.setEmployeeHours(getEmployeeHours));
 }

So the culprit seems to be the yield put(flexHoursActions.setSelectedDates(selectedDates)); and the renders it causes. But I don't understand why there is an issue.

I'm hoping you can enlighten me!

Also, thank you for your work!

/ Jakob

Description

Please provide some context about the problem and some code examples if relevant.
The best way to provide an example is repository or use Code Sandbox(there is a prepared template for Code Sandbox)

@neurosnap
Copy link
Member

neurosnap commented May 23, 2023

Hi! I'd be happy to work through this problem with you.

If I open an Incognito tab without plugins the saga does not seem to lock the UI.

This is my first hunch of a potential problem. What extensions do you have installed (e.g. react-devtools and redux-devtools)? It is not uncommon for these tools to lock the UI thread because they are inspecting the react component tree and observing every single redux action being dispatched. I would try uninstalling them one-by-one to see if we can figure out which one is causing the UI lock -- it could be both.

// If a "yield delay(1);" is added here the UI is not locked.

Adding yield delay(1) is really just a way to add this saga into a Macrotask queue in the browser, which could help with UI locking in certain scenarios. This is especially interesting for redux-saga because sagas can and will run synchronously if there are no async operations being called. Feel free to consult our cheatsheet on which operations are blocking.

// This is a pure redux action, only updates the state selectedDates
// Alot of heavy renders are triggered by this, the UI seems to lock until these renders are resolved
yield put(flexHoursActions.setSelectedDates(selectedDates));

I would also investigate this action. Are you certain that no other sagas are being triggered here? It seems you already know that this is causing a lot of rendering, this might be a good place to try to figure out how to optimize performance.

Finally, I would try to monitor performance while this task is running. The "Performance" tab in the Chrome devtools is a great resource for this. It should help you pinpoint what is causing the UI thread to lock.

Hopefully that gives you enough to continue your investigation, good luck, and feel free to keep chatting here!

@jakobthomasson
Copy link
Author

Thank you for your time !

... I would try uninstalling them one-by-one to see if we can figure out which one is causing the UI lock -- it could be both.

I did what you suggested regarding the plugins, the culprit is not a single plugin but an accumulation. The biggest culprit is LastPass for some reason.

Adding yield delay(1) is really just a way to add this saga into a Macrotask queue in the browser, which could help with UI locking in certain scenarios. This is especially interesting for redux-saga because sagas can and will run synchronously if there are no async operations being called.

This goes hand in hand with my current understanding aswell. I think there could be some nuance here which will make me understand the behaviour I'm seeing.

<button 
    onClick={() => {
      console.log("1");
      onClose(); // when state updates: console.log("2");
      console.log("3");
      startSaveSelectedDates(dates);
      console.log("4");
    } 
/>
  
function* startSelectDatesSaga(
  action: ActionType<typeof flexHoursActions.startSelectDates>,
) { 
  console.log("saga 1");
  const { selectedDates } = action.payload;

  yield put(flexHoursActions.setSelectedDates(selectedDates));
  console.log("saga 2");
  
  const {
    employeeHours,
  }: SagaReturnType<typeof actualAPI.getEmployeeHours> = yield call(
    actualAPI.getEmployeeHours,
    {   
      dates: selectedDates,
    },
  );
  console.log("saga 3");
  yield put(employeeHourActions.setEmployeeHours(getEmployeeHours));
  console.log("saga 4");
 }

The console says: 1, 3, saga 1, saga 2, 4, 2, saga 3, saga 4 where saga 2 happens immediatly after saga 1.

In my mind I would expect 1, 2, 3, saga 1, saga 2, 4, saga 3, saga 4

And even with the yield delay(1) the flow looks:
1, 3, 4, 2, saga 1, saga 2, saga 3, saga 4 where I would expect 1, 2, 3, 4, saga 1, saga 2, saga 3, saga 4

I guess why I'm not seeing 1, 2, 3 must be because of how react batches renders together.
And the reason why the UI locks is just because of that, react tryes to render the "is modal open" update together with the big render caused by: yield put(flexHoursActions.setSelectedDates(selectedDates));. And the first time the UI unlocks is when the api call happens, that's why we see this behaviour.

So basically is what I wrote above how it works? If yes, the plugins slows down the rendering so much so that I experience the issue.

Thank you again, sorry if it ended up a bit confusing.

@neurosnap
Copy link
Member

neurosnap commented May 30, 2023

In your onClick you are referencing startSaveSelectedDates(dates); which is a saga. Is that actually how you are calling that function? If so then it will not work as expected. You need to dispatch an action that then calls startSaveSelectedDates. This is usually done with a watcher saga that calls takeEvery:

import { takeEvery } from 'redux-saga/effects';

function* watchSelectedDates() {
  yield takeEvery("START_SELECTED_DATES", startSaveSelectedDates);
}

// then in react component
function App() {
  const dispatch = yield* useDispatch();
  const onClick = () => {
    dispatch({ type: "START_SELECTED_DATES", payload: dates });
  }
}

@jakobthomasson
Copy link
Author

jakobthomasson commented May 30, 2023

Hello again !

the onClick is dispatching an action.
In my watcher function I'm using takeLatest not takeEvery

I'm positive that I do everything correctly regarding how actions are dispatched.
The code in my previous comment is just for explanation.

I bleieve this is just a understanding issue from my part.

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

No branches or pull requests

2 participants