Material-UI Progress hickups - css animation stops running

Viewed 1478

I'm implementing a React application which renders dynamic data grids. In a recent requirement I've integrated MaterialUI Progress component to give feedback to the user when the table is loading/rendering data. I've used the Progress Component in indeterminate mode as I do not know how long the data request and page redraw will take.

Now for the problem: The progress indicator works well during the time of the data request, however during rendering - which might take some 100ms depending on the number of cells displayed the progress indicator just does not animate.

From my research I've found the progress indicators use css keyframe animations. I was not aware the keyframe animations do just stop during busy rendering. In an experiment I've included a plain old gif animation spinner which seems to continue running during busy rendering.

Can anyone please elaborate on the reasons why the gif animation works well while the keyframe animation just stops running? Is there a simple way around this problem? Using a gif seems viable however this just feels a bit outdated and not as flexible as the css approach. How can I optimize my application in order to show a smooth loading animation even if rendering runtime of a component is taking long.

Thanks for your help, Martin

1 Answers

Animations that require a document reflow (changing margin, padding, positioning), tend to freeze when heavy calculation is going on.
The material progress bars make use of these properties to achieve their animations.
They've described this on their limitations section.

There are 3 workarounds:

  1. Animations that use transform functions aren't affected by heavy calculations
  2. Move the Heavy calculation of the render thread, using a web worker.
  3. Batch the calculation using setTimeouts, so the render thread has time to animate.

Material's circularProgress has an animation using transform. you could try to use that like :
<CircProgress disableShrink={true} />

or try moving the heavy calculations to a webworker. You should decide for yourself if that's feasable.

This is an example using both methods
It's using 'react-webworker-hook' package to make the webworker is bit more accessible.

import React, { useState } from "react";
import LinProgress from "@material-ui/core/LinearProgress";
import CircProgress from "@material-ui/core/CircularProgress";
import { useWebWorkerFromScript } from "react-webworker-hook";

const slowFunc = () => {
  var start = +new Date();
  while (+new Date() - start < 2000) {}
  return Math.random();
};

const MyComponent = (props) => {
  const [inThreadData, setInThreadData] = useState(0);
  //startHeavyRender: will block the render thread, animations will pause
  const startHeavyRender = React.useMemo(
    () => () => setInThreadData(slowFunc),
    []
  );

  //postData: will do the same calculation using a webworker, animations will continue
  const [workerData = 0, postData] = useWebWorkerFromScript(`
  const slowFunc = () => {
    var start = +new Date();
    while (+new Date() - start < 2000) {}
    return Math.random();  
  };

  onmessage = ({ data }) => {
    postMessage(slowFunc());
  };
  
  `);

  const startWorkerRender = React.useMemo(() => () => postData(), [postData]);

  return (
    <div>
      <LinProgress />
      <br />
      <CircProgress />
      <br />
      <CircProgress disableShrink={true} />
      <br />
      <button onClick={startHeavyRender}>
        start heavy render. {inThreadData}
      </button>
      <br />
      <button onClick={startWorkerRender}>
        start heavy webworker. {workerData}
      </button>
    </div>
  );
};

export default MyComponent;
Related