Giter Site home page Giter Site logo

react-performance's Introduction

React is fast, until it isn't

Learn everything you need to diagnose, profile, and fix performance problems in your React application using the Browser Performance Profiler, React DevTools Profiler, and proven React optimization techniques.

Learn React from Start to Finish

Build Status All Contributors GPL 3.0 License Code of Conduct

Prerequisites

  • Install the React DevTools (Chrome (recommended), Firefox)
  • Experience with React and all hooks

System Requirements

  • git v2.13 or greater
  • NodeJS 12 || 14 || 15 || 16
  • npm v6 or greater

All of these must be available in your PATH. To verify things are set up properly, you can run this:

git --version
node --version
npm --version

If you have trouble with any of these, learn more about the PATH environment variable and how to fix it here for windows or mac/linux.

Setup

If you want to commit and push your work as you go, you'll want to fork first and then clone your fork rather than this repo directly.

After you've made sure to have the correct things (and versions) installed, you should be able to just run a few commands to get set up:

git clone https://github.com/kentcdodds/react-performance.git
cd react-performance
node setup

This may take a few minutes. It will ask you for your email. This is optional and just automatically adds your email to the links in the project to make filling out some forms easier.

If you get any errors, please read through them and see if you can find out what the problem is. If you can't work it out on your own then please file an issue and provide all the output from the commands you ran (even if it's a lot).

If you can't get the setup script to work, then just make sure you have the right versions of the requirements listed above, and run the following commands:

npm install
npm run validate

If you are still unable to fix issues and you know how to use Docker 🐳 you can setup the project with the following command:

docker-compose up

It's recommended you run everything locally in the same environment you work in every day, but if you're having issues getting things set up, you can also set this up using GitHub Codespaces (video demo) or Codesandbox.

Running the app

To get the app up and running (and really see if it worked), run:

npm start

This should start up your browser. If you're familiar, this is a standard react-scripts application.

You can also open the deployment of the app on Netlify.

Running the tests

npm test

This will start Jest in watch mode. Read the output and play around with it. The tests are there to help you reach the final version, however sometimes you can accomplish the task and the tests still fail if you implement things differently than I do in my solution, so don't look to them as a complete authority.

Exercises

  • src/exercise/00.md: Background, Exercise Instructions, Extra Credit
  • src/exercise/00.js: Exercise with Emoji helpers
  • src/__tests__/00.js: Tests
  • src/final/00.js: Final version
  • src/final/00.extra-0.js: Final version of extra credit

The purpose of the exercise is not for you to work through all the material. It's intended to get your brain thinking about the right questions to ask me as I walk through the material.

Helpful Emoji 🐨 πŸ’° πŸ’― πŸ“ πŸ¦‰ πŸ“œ πŸ’£ πŸ’ͺ 🏁 πŸ‘¨β€πŸ’Ό 🚨

Each exercise has comments in it to help you get through the exercise. These fun emoji characters are here to help you.

  • Kody the Koala 🐨 will tell you when there's something specific you should do version
  • Marty the Money Bag πŸ’° will give you specific tips (and sometimes code) along the way
  • Hannah the Hundred πŸ’― will give you extra challenges you can do if you finish the exercises early.
  • Nancy the Notepad πŸ“ will encourage you to take notes on what you're learning
  • Olivia the Owl πŸ¦‰ will give you useful tidbits/best practice notes and a link for elaboration and feedback.
  • Dominic the Document πŸ“œ will give you links to useful documentation
  • Berry the Bomb πŸ’£ will be hanging around anywhere you need to blow stuff up (delete code)
  • Matthew the Muscle πŸ’ͺ will indicate that you're working with an exercise
  • Chuck the Checkered Flag 🏁 will indicate that you're working with a final
  • Peter the Product Manager πŸ‘¨β€πŸ’Ό helps us know what our users want
  • Alfred the Alert 🚨 will occasionally show up in the test failures with potential explanations for why the tests are failing.

Notes

Code Splitting - Code splitting acts on the principle that loading less code will speed up your app. Say for example that we’re building a complex dashboard application that includes the venerable d3 library for graphing data. Your users start complaining because it takes too long to load the login screen.

Built-in javascript dynamic import

import('/some-module.js').then(
  module => {
    // do stuff with the module's exports
  },
  error => {
    // there was some error loading the module...
  },
)

React way of loading modules

// smiley-face.js
import * as React from 'react'

function SmileyFace() {
  return <div>πŸ˜ƒ</div>
}

export default SmileyFace

// app.js
import * as React from 'react'

const SmileyFace = React.lazy(() => import('./smiley-face'))

function App() {
  return (
    <div>
      <React.Suspense fallback={<div>loading...</div>}>
        <SmileyFace />
      </React.Suspense>
    </div>
  )
}
  • We may also want to try running the production build so you can see what the sizes are like post-minification: Run npm run build and then npm run serve.

  • The component that are going to be lazy loaded need to be exported as default!

  • The <React.Suspense /> component for the current version of React can be anywhere as long it is a parent of the lazy loaded component (it will only work when attempting to load the lazy loaded component). Altough, in the future with concurrent mode, we want to avoid putting it conditional (inside a State for example, because it can take a certain ammount of time to get the fallback to appear)

<div style={{width: 400, height: 400}}>
  <React.Suspense fallback={<div>loading...</div>}>
    {showGlobe ? <Globe /> : null}
  </React.Suspense>
</div>
  • Dev tools tip: In components tab we can select a component and click on the StopWatch button to suspense/unsuspend the component, and see the <React.Suspense /> behavior:

image

  • We can have the <React.Suspense /> wrapping the whole component, this will mean all the lazy loaded components will reuse the same <React.Suspense />, because it will get the first parent, this is good if we want to have multiples components using only one <React.Suspense />

Eager Loading - Technique to pre-load heavy components so the user can have it loaded when he needs (usually using onMouseOver, onMouseEnter or focus we kick off a dynamic import)

<label style={{marginBottom: '1rem'}} onMouseOver={() => import('../globe')}>
  <input
    type="checkbox"
    checked={showGlobe}
    onChange={e => setShowGlobe(e.target.checked)}
  />
  {' show globe'}
</label>

Webpack Magic Components - We can use magic comments to make webpack add things to the document's head and instruct the browser to prefetch dynamic imports

import(/* webpackPrefetch: true */ './some-module.js')

When webpack sees this comment, it adds this to your document’s head:

```

  • With this, the browser will automatically load this JavaScript file into the browser cache so it’s ready ahead of time.

  • We can use the webpackChunkName magic comment which will allow webpack to place common modules in the same chunk. This is good for components which you want loaded together in the same chunk (to reduce multiple requests for multiple modules which will likely be needed together).

const One = React.lazy(() =>
  import(/* webpackChunkName: "group" */ './group/one'),
)
const Two = React.lazy(() =>
  import(/* webpackChunkName: "group" */ './group/two'),
)

The Coverage Tool We can use the browser's coverage tool, to analyze how much code we are loading at the initial load that we are not using

image

  • A tip when doing optimization is to leverage on production code, not development, and compare the previous version with the current, because we might not always have a 100% optmized code splitting

  • The red is the unused code, if we click we will have the code with the lines in red and blue

  • Use incognito to prevent your extensions to affect your coverage

useMemo - Our component's calculations performed within render will be performed every single render, regardless of whether the inputs for the calculations change. For example:

function Distance({x, y}) {
  const distance = calculateDistance(x, y)
  return (
    <div>
      The distance between {x} and {y} is {distance}.
    </div>
  )
}

If that component’s parent re-renders, or if we add some unrelated state to the component and trigger a re-render, we’ll be calling calculateDistance every render which could lead to a performance bottleneck.

This is why we have the useMemo hook from React:

function Distance({x, y}) {
  const distance = React.useMemo(() => calculateDistance(x, y), [x, y])
  return (
    <div>
      The distance between {x} and {y} is {distance}.
    </div>
  )
}
  • Also, in React docs, it says is not always guaranteed the value to be memoized, because React can choose to free some memory by getting rid of some memoized values, but usually, we don't need to take that into account.

  • A good aim for performance, is to have 60 frames a second, which is where we have a nice smooth experience for the human eye, then you need to nail 16 frames a second, that is 1,000 divided by 60 frames a second. That's going to be 16 milliseconds, thereabouts, for your JavaScript to run so that the browser can keep up and not drop any frames and resulting in a janky experience.

  • So we aim, our JavaScript to run in 16 milliseconds, or it's still not super awesome.

Always compare runing production mode : npm run build then npm run serve

Web Workers - We can use Web Workers to put slow functions run in a thread appart from the main thread, that can help us with performance because the main thread is not interrupted to execute the slow function, we just need a way to work asynchronous with the web worker's data

const {data: allItems, run} = useAsync()

React.useEffect(() => {
  run(getItems(inputValue))
}, [inputValue, run])
  • workerize is a package that makes using web workers easier: and have the workerize-loader which is a webpack loader for workerize which basically means you can put any module (and the modules that it imports) into a webworker.
// eslint-disable-next-line import/no-webpack-loader-syntax
import makeFilterCitiesWorker from 'workerize!./filter-cities'
const {getItems} = makeFilterCitiesWorker()
export {getItems}
  • The workerize! thing in the import statement is a fancy webpack syntax to tell webpack to treat that module specially (specifically to pipe it through the workerize-loader so Jason can do his magic on it to get it into a web worker).

Lifecycle of React

β†’  render β†’ reconciliation β†’ commit
         β†–                   ↙
              state change
  • β€œrender” phase: create React elements React.createElement
  • β€œreconciliation” phase: compare previous elements with the new ones
  • β€œcommit” phase: update the DOM (if needed).

A React Component can re-render for any of the following reasons:

  • Its props change
  • Its internal state changes
  • It is consuming context values which have changed
  • Its parent re-renders

We can opt-out of state updates for a part of the React tree by using one of React’s built-in rendering bail-out utilities: React.PureComponent, React.memo, or shouldComponentUpdate.

Using one of the bail-out APIs, you can instruct React when to re-render. React.PureComponent is for class components and React.memo is for function components and they do basically the same thing by default. They make it so your component will not re-render simply because its parent re-rendered which could improve the performance of your app overall.

React.memo example:

function CountButton({count, onClick}) {
  return <button onClick={onClick}>{count}</button>
}

function NameInput({name, onNameChange}) {
  return (
    <label>
      Name: <input value={name} onChange={e => onNameChange(e.target.value)} />
    </label>
  )
}

function Example() {
  const [name, setName] = React.useState('')
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(c => c + 1)
  return (
    <div>
      <div>
        <CountButton count={count} onClick={increment} />
      </div>
      <div>
        <NameInput name={name} onNameChange={setName} />
      </div>
      {name ? <div>{`${name}'s favorite number is ${count}`}</div> : null}
    </div>
  )
}

Based on how this is implemented, when you click on the counter button, the <CountButton /> re-renders (so we can update the count value). But the <NameInput /> is also re-rendered. If you have Record why each component rendered while profiling. enabled in React DevTools, then you’ll see that under β€œWhy did this render?” it says "The parent component rendered."

function CountButton({count, onClick}) {
  return <button onClick={onClick}>{count}</button>
}

function NameInput({name, onNameChange}) {
  return (
    <label>
      Name: <input value={name} onChange={e => onNameChange(e.target.value)} />
    </label>
  )
}
NameInput = React.memo(NameInput)

// etc... no other changes necessary

Alternative ways of using React.memo:

const NameInput = React.memo(({name, onNameChange}) => {
  return (
    <label>
      Name: <input value={name} onChange={e => onNameChange(e.target.value)} />
    </label>
  )
})
// But this is not so good because it won't print the name of the function in the Dev tools
const NameInput = React.memo(function NameInput({name, onNameChange}) {
  return (
    <label>
      Name: <input value={name} onChange={e => onNameChange(e.target.value)} />
    </label>
  )
})
// This will print the name of the function in the Dev tools, but does not looks nice
  • React.memo accepts a second argument which is a custom compare function that allows us to compare the props and return true if rendering the component again is unnecessary and false if it is necessary.

  • If it returns true, it won't rerender, if returns false it will

Without the first argument, the default behaviour of React.memo is to compare each previous prop againts the next prop

I.e:

// 🐨 Memoize the ListItem here using React.memo
ListItem = React.memo(ListItem, (prevProps, nextProps) => {
  // rerender if the highlightindex is the current one (to apply current item selection)
  /*
  // Default behavior:
  if (prevProps.getItemProps !== nextProps.getItemProps) return false  
  if (prevProps.items !== nextProps.items) return false  
  if (prevProps.index !== nextProps.index) return false  
  if (prevProps.selectedItem !== nextProps.selectedItem) return false  
  if (prevProps.highlightedIndex !== nextProps.highlightedIndex) return false  
  return true
  */
  if (nextProps.highlightedIndex === nextProps.index) {
    return false
  }
  // rerender if the highlightexindex was the previous one (to clear previous item selection)
  if (prevProps.highlightedIndex === prevProps.index) {
    return false
  }

  // otherwise, don't need to rerender
  return true
})

Note: Even better is to not have to provide our custom memoization comparator function and still get the perf gains? Definitely! So an alternative approach is to pass the pre-computed values for isSelected and isHighlighted to our ListItem. That way they are primitive values and we can take advantage of React’s built-in comparison function and remove ours altogether.

<ul {...getMenuProps()}>
  {items.map((item, index) => {
    const isSelected = selectedItem?.id === item.id
    const isHighlighted = highlightedIndex === index

    return (
      <ListItem
        key={item.id}
        getItemProps={getItemProps}
        item={item}
        index={index}
        // selectedItem={selectedItem}
        isSelected={isSelected}
        isHighlighted={isHighlighted}
        // highlightedIndex={highlightedIndex}
      >
        {item.name}
      </ListItem>
    )
  })}
</ul>

Useful Profiler Tips

Enable Why each component rendered while profiling

Click on the Cog Icon to go to settings
Example showing why did this render
The chart columns displayed are the commits

Premises

  • "Fix the slow render before you fix the unnecessary rerender" Kent C Dodds

  • Avoid the mistake of wrapping everything in React.memo which can actually slow down your app in some cases and in all cases it makes your code more complex.

Windowing

  • We can use react-virtual to create a virtualized list with react
  • The idea of virtualization is to render only a subset of the list items. This is useful when the list is very long and you don’t need to render all of it.
  • We also play with calculated style heights to keep using the original scrollbar

Context

The way context works is that whenever the provided value changes from one render to another, it triggers a re-render of all the consuming components (even if they're memoized). Ex.

const CountContext = React.createContext()

function CountProvider(props) {
  const [count, setCount] = React.useState(0)
  const value = [count, setCount]
  return <CountContext.Provider value={value} {...props} />
}

Every time the <CountProvider /> re-renders, the value is brand new, so all consumers will be re-rendered. The easiest way to avoid this is to use React.useMemo():

const CountContext = React.createContext()

function CountProvider(props) {
  const [count, setCount] = React.useState(0)
  const value = React.useMemo(() => [count, setCount], [count])
  return <CountContext.Provider value={value} {...props} />
}
  • Note: We can have multiple contexts wrapped by a single component, because, only the specific consumers will get re-rendered.
function AppProvider({children}) {
  const [state, dispatch] = React.useReducer(appReducer, {
    dogName: '',
    grid: initialGrid,
  })
  return (
    <AppStateContext.Provider value={state}>
      <DispatcherContext.Provider value={dispatch}>
        {children}
      </DispatcherContext.Provider>
    </AppStateContext.Provider>
  )
}

// Only the specifics consumers for AppStateContext and DispatcherContext will get re-rendered when state or dispatch changes

Performance death by a thousand cuts: when lots of components need to run when there’s a state update, which basically means that so many components are updated when state changes that it becomes a performance bottleneck.

Usually it can be solved with less code, reducing component complexity, or often you have components responding to state change that don't need to. We can memoize with React.memo or we can use colocation, and move state inside or closer as possible with the components instead of placing in a global state.

  • Another alternative solution is to split logic into two differents context, if for some reason we need to have a global state.
  • Another solution is create a man in the middle component, which will consume the state and have the Impl component memoized, so it will only rerender if needed!
function Cell({row, column}) {
  const state = useAppState()
  const cell = state.grid[row][column]

  return <CellImpl cell={cell} row={row} column={column} />
}
Cell = React.memo(Cell)

function CellImpl({cell, row, column}) {
  const dispatch = useAppDispatch()
  const handleClick = () => dispatch({type: 'UPDATE_GRID_CELL', row, column})

  return (
    <button
      className="cell"
      onClick={handleClick}
      style={{
        color: cell > 50 ? 'white' : 'black',
        backgroundColor: `rgba(0, 0, 0, ${cell / 100})`,
      }}
    >
      {Math.floor(cell)}
    </button>
  )
}
CellImpl = React.memo(CellImpl)
  • A more generic solution using HOC would be:
function withStateSlice(Component, slice) {
  const MemoizedComponent = React.memo(Component)
  function Wrapper(props) {
    const state = useAppState()
    return <MemoizedComponent state={slice(state, props)} {...props} />
  }

  Wrapper.displayName = `withStateSlice(${
    Component.displayName || Component.name
  })`
  return React.memo(Wrapper)
}

function Cell({state: cell, row, column}) {
  const dispatch = useAppDispatch()
  const handleClick = () => dispatch({type: 'UPDATE_GRID_CELL', row, column})

  return (
    <button
      className="cell"
      onClick={handleClick}
      style={{
        color: cell > 50 ? 'white' : 'black',
        backgroundColor: `rgba(0, 0, 0, ${cell / 100})`,
      }}
    >
      {Math.floor(cell)}
    </button>
  )
}
Cell = withStateSlice(Cell, (state, {row, column}) => state.grid[row][column])
  • Also, alternatively, we can use Recoil, to create small state slices. Example

Avoiding performance regression

  • Lighthouse CI is a tool that runs a series of audits on a page and reports the results.

  • Reference

    NOTE: for any of this to work in production, you need to enable React's profiler build. There's a small performance cost for doing this, so facebook.com only serves the profiler build of their app to a subset of users. Learn more about how to enable the profiler build from Profile a React App for Performance.

  • We can use React built-in function React.Profiler to profile our components and keep tracking of performance measurements. The general strategy for the onRender function is that it should send events in batches to some backend monitoring service. I.e Graphana or Google Analytics.

function App() {
  return (
    <div>
      {/*
      🐨 Wrap this div in a React.Profiler component
      give it the ID of "counter" and pass reportProfile
      to the onRender prop.
      */}
      <React.Profiler id="counter" onRender={reportProfile}>
        <div>
          Profiled counter
          <Counter />
        </div>
      </React.Profiler>
      <div>
        Unprofiled counter
        <Counter />
      </div>
    </div>
  )
}
  • The data it collects is: id, phase, actualDuration, baseDuration, startTime, commitTime, interactions.

  • The actualDuration is the time spent rendering the component.

  • The startTime is the time when the component begin rendering.

  • The baseDuration is the time spent rendering the component tree without memoization.

  • The commitTime is the time when the component commited the update.

Contributors

Thanks goes to these wonderful people (emoji key):


Kent C. Dodds

πŸ’» πŸ“– πŸš‡ ⚠️

Justin Dorfman

πŸ”

FrΓ©dΓ©ric Bolvin

πŸ“–

Vojta Holik

πŸ’» 🎨

Marco Moretti

πŸ’»

Ricardo Busquet

πŸ’»

Selwyn Yeow

πŸ’»

Emmanouil Zoumpoulakis

πŸ“–

Peter HozΓ‘k

πŸ’»

Pritam Sangani

πŸ’»

Christian Schurr

πŸ’» πŸ“–

Johnny Magrippis

πŸ’»

Ahmed

πŸ’» πŸ“–

Robbert Wolfs

πŸ“–

Kim Gwan-duk

πŸ“–

Rasmus Josefsson

πŸ’»

Marcos NASA G

πŸ“–

Markus Lasermann

πŸ“–

Vasilii Kovalev

πŸ“–

Matan Kushner

πŸ“–

MichaΓ«l De Boey

πŸ’»

Veljko Blagojevic

πŸ“–

Bobby Warner

πŸ’»

JesΓΊs RodrΓ­guez

πŸ“–

Valentin Hervieu

πŸ›

David SΓ‘nchez

πŸ“–

Merott Movahedi

πŸ“–

Arjen Bloemsma

πŸ“–

Naveed Elahi

πŸ“–

Tyler Knappe

πŸ“–

0xnoob

πŸ’» πŸ“–

This project follows the all-contributors specification. Contributions of any kind welcome!

Workshop Feedback

Each exercise has an Elaboration and Feedback link. Please fill that out after the exercise and instruction.

At the end of the workshop, please go to this URL to give overall feedback. Thank you! https://kcd.im/rp-ws-feedback

react-performance's People

Contributors

opauloh avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.