Writing a Slow
React App 🐌

ABOUT ME 👨🏻‍💻

Sudhanshu Yadav

  • Front-end Architect at HackerRank.
  • Author of Brahmos, react-number-format,
    packagebind and other OSS projects.
  • Organizes The Internals.

AGENDA 📋

What and Why?

  • Patterns leading to a performance bottleneck.
  • Why these patterns are slow in React?
  • How to fix them?
  • Will talk about React Internals & Micro-optimization.
  • Will see lots of code today.

Expensive Renders

// bigList can have 1000's of items
function BigRender({ bigList }) {
  return (
    <div>
      <h3>Big List</h3>
      <ul className="items">
        {bigList.map(item => (
          <li className="item" key={item.id}>
            <h4 className="item-title">{item.title}</h4>
            <p>{item.description}</p>
            {/*... */}
          </li>
        ))}
      </ul>
    </div>
  );
}

Big Render methods

// bigList can have 1000's of items
function BigRender({ bigList }) {
  return (
    <div>
      <h3>Big List</h3>
      <ul className="items">
        <Virtualize items={bigList}>
          {item => (
            <li className="item" key={item.id}>
              <h4 className="item-title">{item.title}</h4>
              <p>{item.description}</p>
              {/*... */}
            </li>
          )}
        </Virtualize>
      </ul>
    </div>
  );
}

VirTUALIZE

How React RENDERING WORKS?

function Greet({ name }) {
  return (
    <div className="greeting">
      Hello <FancyName name={name} /> !!
    </div>
  );
}

You write in JSX.

function Greet({ name }) {
  return React.createElement(
    "div", //element type
    { className: "greeting" }, //props

    // followed by children
    "Hello ",
    React.createElement(FancyName, { name: name }),
    " !!"
  );
}

JSX is transformed to createElement

{
  type: "div",
  props: {
    className: "greeting",
    children: [
      "Hello ",
      {
        type: FancyName,
        props: { name: name }
      },
      " !!"
    ]
  }
};

createElement returns a data structure (VDOM)

// fiber structure
{
  type: NodeConstructor, // div, FancyName
  stateNode: NodeInstance, // HTMLDivElement, FancyNameInstance
  child: childFiber,
  sibling: siblingFiber,
  return: parentFiber,
}

reactElements are mapped to a Fiber node.

{
  type: "div", // <- parent
  props: {
    className: "greeting",
    children: [
      "Hello ", // <- Child text head
      {
        // Child FancyName (sibling of text head)
        type: FancyName,
        props: { name: name }
      },
      " !!" // <- child text tail (sibling of FancyName)
    ]
  }
}

React process each fiber node one at a time and can pause in middle.

// bigList can have 1000's of items
function BigRender({ bigList }) {
  return (
    <div>
      <h3>Big List</h3>
      <ul className="items">
        {bigList.map(item => (
          <li className="item" key={item.id}>
            <h4 className="item-title">{item.title}</h4>
            <p>{item.description}</p>
            {/*... */}
          </li>
        ))}
      </ul>
    </div>
  );
}

Big Render methods

function ListItem({ item }) {
  return (
    <li className="item">
      <h4 className="item-title">{item.title}</h4>
      <p>{item.description}</p>
      {/*... */}
    </li>
  );
}

function BigRender({ bigList }) {
  return (
    <div>
      <h3>Big List</h3>
      <ul className="items">
        {bigList.map(item => (
          <ListItem key={item.id} item={item} />
        ))}
      </ul>
    </div>
  );
}

BReAK THE COMPONENT

function ExpensiveRender({ data }) {
  const value = someExpensiveCalculation(data);
  return (
    <div>
      <h3>Expensive Calculation</h3>
      <div className="expansive-value">{value}</div>
    </div>
  );
}

Expensive Render

function ExpensiveRender({ data }) {
  const memoizedValue = useMemo(() => someExpensiveCalculation(data), [data]);
  return (
    <div>
      <h3>Expensive Calculation</h3>
      <div className="expansive-value">{memoizedValue}</div>
    </div>
  );
}

Memoize COMPUTATION

CANCELLING THE DIFFING

function TestComponent({ factor, initialValue }) {
  const [incrementedValue, increment] = useState(initialValue);

  const onClick = () => {
    increment(incrementedValue + factor);
  };

  const IncrementResult = () => (
    <FancyResult>
      <FancyResult.label>Incremented with {factor}</FancyResult.label>
      <FancyResult.value>{incrementedValue}</FancyResult.value>
    </FancyResult>
  );

  return (
    <>
      <button className="action" onClick={onClick}>
        Action
      </button>
      <IncrementResult />
    </>
  );
}

INLINE COMPONENTS

The Diffing Algorithm

  • On rerender compare the previous tree with the new tree.
  • For each element
    • If a key is provided compare the element with the previous element with the same key.
    • If the key is not provided compare the element with the previous element at the same Index.
    • If a key is provided but there is no previous element with the same key compare it with null.

The Diffing Algorithm

Compare:

  • If the type of the element is the same as the previous element's type, update the component.
  • If the type is different remove the previous element and add the new one.
function TestComponent({ factor, initialValue }) {
  const [incrementedValue, increment] = useState(initialValue);

  const onClick = () => {
    increment(incrementedValue + factor);
  };

  const IncrementResult = () => (
    <FancyResult>
      <FancyResult.label>Incremented with {factor}</FancyResult.label>
      <FancyResult.value>{incrementedValue}</FancyResult.value>
    </FancyResult>
  );

  return (
    <>
      <button className="action" onClick={onClick}>
        Action
      </button>
      <IncrementResult />
    </>
  );
}

INLINE COMPONENTS

const IncrementResult = ({factor, value}) => (
  <FancyResult>
    <FancyResult.label>Incremented with {factor}</FancyResult.label>
    <FancyResult.value>{value}</FancyResult.value>
  </FancyResult>
);

function TestComponent({ factor, initialValue }) {
  const [incrementedValue, increment] = useState(initialValue);

  const onClick = () => {
    increment(incrementedValue + factor);
  };

  return (
    <>
      <button className="action" onClick={onClick}>
        Action
      </button>
      <IncrementResult factor={factor} value={value}/>
    </>
  );
}

STABLE COMPONENT REFERENCE

function withProps(Component, fixedProps) {
  return props => {
    <Component {...fixedProps} {...props} />;
  };
}

function TestComponent({ factor, initialValue }) {
  const [incrementedValue, increment] = useState(initialValue);

  const IncrementResultWithProps = withProps(IncrementResult, { factor });

  return (
    <>
      <button className="action" onClick={onClick}>
        Action
      </button>
      <IncrementResultWithProps incrementedValue={incrementedValue} />
    </>
  );
}

CREATING HOC INSIDE COMPONENT

function WrongKeyImplementation({ person }) {
  const { name } = person;

  return (
    <div className="person">
      <div className="name">{person.name}</div>
      <PersonDescription key={person.name} person={person} />
    </div>
  );
}

// 1st render
<WrongKeyImplementation person={{name: 'Sudhanshu', ...otherInfo}} />

// 2nd render
<WrongKeyImplementation person={{name: 'Yadav', ...otherInfo}} />

INCORRECT USE OF KEY

function WrongKeyImplementation({ person }) {
  const { name } = person;

  return (
    <div className="person">
      <div className="name">{person.name}</div>
      <PersonDescription person={person} />
    </div>
  );
}

// 1st render
<WrongKeyImplementation person={{name: 'Sudhanshu', ...otherInfo}} />

// 2nd render
<WrongKeyImplementation person={{name: 'Yadav', ...otherInfo}} />

USE KEY ONLY WHEN REQUIRED

function Items({ items }) {
  const [orderedItems, setItems] = useState(items);

  return (
    <div>
      <button onClick={() => setItems(shuffle(orderedItems))}>Shuffle</button>
      {orderedItems.map((item, idx) => (
        <Item key={idx} item={item} />
      ))}
    </div>
  );
}

USING INDEX AS KEY

function Items({ items }) {
  const [orderedItems, setItems] = useState(items);

  return (
    <div>
      <button onClick={() => setItems(shuffle(orderedItems))}>Shuffle</button>
      {orderedItems.map((item) => (
        <Item key={item.id} item={item} />
      ))}
    </div>
  );
}

USE DETERMINISTIC KEY

COST OF HIERARCHY

function PlainComponent({ theme, utils, i18n }) {
  return <div>{/** ...content... */}</div>;
}

export default withTheme(withUtils(withI18n(PlainComponent)));

HighER ORDER COMPONENT

function PlainComponent() {
  return (
    <Theme>
      {theme => (
        <Utils>
          {utils => (
            <I18N>
              {i18n => (
                <div>{/** ...content... */}</div>
              )}
            </I18N>
          )}
        </Utils>
      )}
    </Theme>
  );
}

Render PROPS

function PlainComponent(props) {
  const theme = useTheme();
  const utils = useUtils();
  const i18n = useI18n(props);

  return <div>{/** ...content... */}</div>;
}

Hooks

function Title(props) {
  return <h1 {...props} />;
}

function Para(props) {
  return <p {...props} />;
}

function Block(props) {
  return <div {...props} />;
}

function SimpleComponent() {
  return (
    <Block>
      <Title>Some Title</Title>
      <Para>Lorem ipsum ......</Para>
    </Block>
  );
}

OvER ABSTRACTION

function SimpleComponent() {
  return (
    <div>
      <h1>Some Title</h1>
      <p>Lorem ipsum ......</p>
    </div>
  );
}

 MEANINGFUL ABSTRACTION

UNOPTIMIZED PROVIDERS & SELECTORS

function Founder({ organization }) {
  const { name: organizationName, team } = organization;
  const { name, bio } = team.founder;

  return (
    <div>
      <div className="organization-name">{organizationName}</div>
      <div className="founder-name">{name}</div>
      <p className="founder-bio">{bio}</p>
    </div>
  );
}

connect(state => {
  return {
    organization: state.organization
  };
})(Founder);

CoNNECTING TOO MUCH DATA

0
 Advanced issue found
function Founder({ organizationName, name, bio }) {
  return (
    <div>
      <div className="organization-name">{organizationName}</div>
      <div className="founder-name">{name}</div>
      <p className="founder-bio">{bio}</p>
    </div>
  );
}

connect(state => {
  const { name: organizationName, team } = state.organization;
  const { name, bio } = team.founder;

  return {
    organizationName,
    name,
    bio
  };
})(Founder);

CoNNECT REQUIRED DATA

0
 Advanced issue found
function Founder() {
  const { organizationName, name, bio } = useSelector(state => {
    const { name: organizationName, team } = state.organization;
    const { name, bio } = team.founder;

    return {
      organizationName,
      name,
      bio
    };
  }, shallowEqual);

  return (
    <div>
      <div className="organization-name">{organizationName}</div>
      <div className="founder-name">{name}</div>
      <p className="founder-bio">{bio}</p>
    </div>
  );
}

WITH USE SELECTOR

0
 Advanced issue found
function BadComponent({ storeState }) {
  return (
    <div>
      {/** ...content... */}
    </div>
  )
}

connect(state => {
  return {
    storeState: state
  };
});

CONNECTING WHOLE STATE

0
 Advanced issue found
function Jobs() {
  const jobs = useSelector(state => {
    const { job, jobIds } = state.jobs;
    return jobIds.map(jobId => job[jobId]);
  });

  return (
    <div>
      {jobs.map(job => (
        <Job jobData={job} key={job.id} />
      ))}
    </div>
  );
}

CREATING NEW REFERENCE

0
 Advanced issue found
function Jobs() {
  const { job, jobIds } = useSelector(state => {
    const { job, jobIds } = state.jobs;
    return { job, jobIds };
  }, shallowEqual);

  const jobs = jobIds.map(jobId => job[jobId]);

  return (
    <div>
      {jobs.map(job => (
        <Job jobData={job} key={job.id} />
      ))}
    </div>
  );
}

MOVE LOGIC OUTSIDE

0
 Advanced issue found
const jobsSelector = createSelector(
  [
    state => state.job,
    state => state.jobIds
  ],
  (job, jobIds) => jobIds.map(jobId => job[jobId])
);

function Jobs() {
  const jobs = useSelector(jobsSelector);

  return (
    <div>
      {jobs.map(job => (
        <Job jobData={job} key={job.id} />
      ))}
    </div>
  );
}

Use RESELECT

const JobsContext = createContext();

function jobsReducer(state, action) {
  //...
}

function App({ children }) {
  const [state, dispatch] = useReducer(jobsReducer);

  return (
    <JobsContext.Provider value={[state, dispatch]}>
      {children}
    </JobsContext.Provider>
  );
}

function JobDescription({ jobId }) {
  // With this JobDescription will rerender even if unrelated job changes 
  const [jobs] = useContext(JobsContext);
  const job = jobs[jobId];
  return <div>{job.name}</div>;
}

UNOPTIMIZED CONTEXT

function JobDescription({ job }) {
  return <div>{job.name}</div>;
}

// Here the withContext can memoize component and 
// rerender JobDescription only if job prop changes
withContext(JobsContext, (context, props) => {
  const [jobs] = context;
  const { jobId } = props;
  return {
    job: jobs[jobId]
  };
})(JobDescription);

CONNECT WITH HOC

UNOPTIMIZED OPTIMIZATION

class ChildPureComponent extends React.PureComponent {
  render() {
    // ...
  }
}

const MemoizedComponent = React.memo(() => {
  //...
});

MEMOIZING COMPONENT

function Example() {
  const [state, setState] = useState("");

  return (
    <div>
      <MemoizedComponent>
        {/* Reference of children (React Element) will always be different. */}
        <span>Hello World</span>
      </MemoizedComponent>
    </div>
  );
}

BREAKING MEMOIZATION

function Example() {
  const [state, setState] = useState("");

  return (
    <div>
      {/* Reference of onChange will always be different. */}
      <MemoizedComponent
        value={state}
        onChange={changes => {
          setState(changes);
        }}
      />
    </div>
  );
}

BREAKING MEMOIZATION

function Example() {
  const [number, setNumber] = useState(0);
  const [factor, setFactor] = useState(0);

  // Will result unnecessary shallow compare if number/factor always changes
  const onSubmit = useCallback(() => {
    setNumber(number + factor);
  }, [number, factor]);

  return (
    <form>
      <div className="value">{number}</div>
      <input value={factor} onChange={e => setFactor(e.target.value)} />
      <button type="submit" onClick={onSubmit}>
        Submit
      </button>
    </form>
  );
}

Wrong uSE oF USE CALLBACK

function Example() {
  const [number, setNumber] = useState(0);
  const [factor, setFactor] = useState(0);

  const onSubmit = () => {
    setNumber(number + factor);
  };

  return (
    <form>
      <div className="value">{number}</div>
      <input value={factor} onChange={e => setFactor(e.target.value)} />
      <button type="submit" onClick={onSubmit}>
        Submit
      </button>
    </form>
  );
}

Don't OVERUSE USE CALLBACK

function MemoExample({ factor, children }) {

  const someExpensiveFactorial = useMemo(() => {
    const childrenCount = React.Children.count(children);

    return computeExpensiveValue(factor, childrenCount);
  }, [factor, children]); /*children reference will always change*/

  return (
    <div>
      <span>Children Factorial: {someExpensiveFactorial}</span>
      {children}
    </div>
  );
}

Incorrect USE OF USE MEMO

function MemoExample({ factor, children }) {
  const childrenCount = React.Children.count(children);

  const someExpensiveFactorial = useMemo(() => {
    return computeExpensiveValue(factor, childrenCount);
  }, [factor, childrenCount]);

  return (
    <div>
      <span>Children Factorial: {someExpensiveFactorial}</span>
      {children}
    </div>
  );
}

MEMOIZE ON STABLE INPUTS

class UpdateCheck extends React.Component {
  shouldComponentUpdate(nextProps) {
    return deepCompare(this.props, nextProps);
  }
  render() {
    return <div>{/* ...contents... */}</div>;
  }
}

Expensive UPDATE CHECK

const UpdateCheck = React.memo(() => {
  return <div>{/* ...contents... */}</div>;
}, deepCompare);

Expensive UPDATE CHECK

THANK YOU 🙏

Writing a Slow React App

By Sudhanshu Yadav

Writing a Slow React App

  • 817