Performance optimization

Performance optimization

topics:

  • visualizing re-renderings in the React devtools
  • measuring render times in the React devtools
  • memoizing expensive calculations for rendering
  • memoizing components based on props
  • memoization and event handlers
  • virtual DOM and the key prop
  • lazy loading of components

Three steps to update a component

Three steps to update a component

  1. build a virtual DOM representation of the new rendering ("render phase")
  2. create a diff between the new and old virtual DOM ("reconciliation phase")
  3. apply any changes in the virtual DOM to the real DOM ("commit phase")

Three steps to update a component

speeding up the three steps:

  • memoizing costly computations (useMemo) - may speed up step 1
  • preventing unneeded component rerenderings - skips all steps if nothing changed
  • using the key property - helps with finding the minimal diff

React devtools and performance

React devtools and performance

react devtools functionality:

  • visually highlight components whenever they update
  • profiler that supports recording and profiling a session

React devtools and performance

highlightling components whenever they update:

In the React devtools settings: select Highlight updates when components render.

Components that render get a colored border (color varies based on render frequency)

React devtools and performance

recording and profiling a session:

In the browser tools' "Profiler" tab:

  • click the record button to start
  • interact with the React application normally (each user action is recorded via a "commit")
  • click the record button to stop

React devtools and performance

exploring the profile data:

Each user interaction (e.g. click, button press) causes a so-called commit

Commits are shown as bars in the top right corner

Details of a commit can be seen by clicking on it

React devtools and performance

Numbers in a commit detail:

TodoApp (3ms of 109ms)

this means:

  • it took 109 milliseconds to render the entire app (note: will be faster in production)
  • most time (106 ms) was spent rendering subcomponents
  • the contents that are specific to TodoApp took 3 ms

React devtools and performance

colors in a commit detail:

Color scale from green to yellow shows how much time a component took to render - compared to its siblings

Grey-striped components did not rerender

Memoization

Memoization

memoization = caching of previously computed results

applications in React:

  • memoization of costly computations
  • memoization of component renderings
  • preserving the identity of event handlers (as basis for memoization of component renderings)

Memoization of costly computations

Memoization of costly computations

example without memoization:

const [todos, setTodos] = useState([]);
const numActiveTodos = todos.filter(
  (todo) => !todo.completed
).length;

Memoization of costly computations

with memoization:

const [todos, setTodos] = useState([]);
const numActiveTodos = useMemo(
  // function to recompute value
  () => todos.filter((todo) => !todo.completed).length,
  // array of dependencies
  [todos]
);

the computation is only rerun if a dependency listed in the array changes

Skipping unneeded rerenders

Skipping unneeded rerenders

Note:

If the rendering of a component is the same as before, re-rendering will generally already be fast as React will only recreate the virtual DOM (and will not touch the real DOM)

Often, no further optimization is necessary

Skipping unneeded rerenders

Generally a component only needs to be rerendered when its props or state actually change

Skipping unneeded rerenders

what React already does for us:

hooks (state, reducer, context) will not trigger a re-rendering if their value has not changed

what we can add:

if a parent component rerenders, but the child's props haven't changed, don't rerender the child component (memoization)

Skipping unneeded rerenders

demo: component only rerenders if its state changes

function Coin() {
  const [coin, setCoin] = useState('heads');
  const throwCoin = () => {
    setCoin(Math.random() > 0.5 ? 'heads' : 'tails');
  };
  return (
    <div>
      {coin}
      <button onClick={throwCoin}>throw</button>
      <div>last rendering: {new Date().toISOString()}</div>
    </div>
  );
}

Skipping unneeded rerenders

if only those subcomponents whose props have changed should rerender:

  • for function components: use React's memo function
  • for class components: inherit from PureComponent instead of Component

Skipping unneeded rerenders

optimized function component:

import { memo } from 'react';

function Rating(props) {
  // ...
}

export default memo(Rating);

optimized class component:

import { PureComponent } from 'react';

class Rating extends PureComponent {
  // ...
}

Skipping unneeded rerenders

the Rating component will not be rerendered if its props are the same as before:

<Rating stars={prodRating} />
<Rating stars={prodRating} onChange={setProdRating} />

Skipping unneeded rerenders

Demo: Sierpinski carpet with and without memo

Skipping unneeded rerenders

See also:

Skipping rerenders and event handlers

Skipping rerenders and event handlers

if Rating is a "memoized" component, which of the following will re-render when the parent is re-rendered?

<Rating stars={prodRating} />
<Rating stars={prodRating} onChange={setProdRating} />
<Rating
  stars={prodRating}
  onChange={(newRating) => setProdRating(newRating)}
/>

Skipping rerenders and event handlers

<Rating
  stars={prodRating}
  onChange={(newRating) => setProdRating(newRating)}
/>

the change handler would be recreated and passed down as a different object on every rendering of the parent component

Skipping rerenders and event handlers

solutions:

  • memoize the event handlers
  • pass down a dispatch function
  • define the event handlers to be passed down in a class component

Skipping rerenders and event handlers

memoizing event handlers:

function TodoApp() {
  const [todos, setTodos] = useState([]);

  const deleteTodoA = useMemo(
    () => (id) => {
      setTodos((todos) => todos.filter((t) => t.id !== id));
    },
    []
  );

  const deleteTodoB = useCallback((id) => {
    setTodos((todos) => todos.filter((t) => t.id !== id));
  }, []);
}

Virtual DOM

Virtual DOM

If a React component does rerender, its results are not directly passed on to the browser.

Instead, a virtual DOM representation is created and compared to the previous virtual DOM. Only the differences are passed on to the browser to process.

Virtual DOM and repeating elements

Usually React is very efficient at figuring out what has changed - but it needs help when elements are repeated in an array

Rule of thumb: Any time we use .map in our JSX templates the inner elements should have a unique key property to help React

Virtual DOM

see also: https://reactjs.org/docs/reconciliation.html

Lazy-loading components

Lazy-loading components

to reduce bundle size of React apps: only import components when they are needed

common use case: import a route only when it is accessed

Lazy-loading components

imports in JavaScript:

  • using import as a statement - synchronous import before the rest of the file is executed (in webpack: automatically included in bundle)
  • using import as a function - asynchronous import when needed

Lazy-loading components

React facilities for lazy-loading:

  • lazy function
  • Suspense component

Lazy-loading components

with react router:

import { Suspense, lazy } from 'react';
import { Route } from 'react-router-dom';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  );
}
source: Route-based code splitting on reactjs.org