{"id":21241200,"url":"https://github.com/icyjoseph/react-performance-study","last_synced_at":"2025-07-13T15:34:15.336Z","repository":{"id":37739020,"uuid":"162322034","full_name":"icyJoseph/react-performance-study","owner":"icyJoseph","description":"Studying React Performance Improvements","archived":false,"fork":false,"pushed_at":"2022-12-10T01:02:58.000Z","size":387,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-15T03:44:42.787Z","etag":null,"topics":["react","react-bootstrap","react-lazy","react-memo","react-performance","reactjs","webpack-bundle-analyzer"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/icyJoseph.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-12-18T17:15:21.000Z","updated_at":"2023-02-12T14:22:30.000Z","dependencies_parsed_at":"2023-01-26T01:16:47.366Z","dependency_job_id":null,"html_url":"https://github.com/icyJoseph/react-performance-study","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/icyJoseph/react-performance-study","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyJoseph%2Freact-performance-study","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyJoseph%2Freact-performance-study/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyJoseph%2Freact-performance-study/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyJoseph%2Freact-performance-study/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/icyJoseph","download_url":"https://codeload.github.com/icyJoseph/react-performance-study/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/icyJoseph%2Freact-performance-study/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":265163457,"owners_count":23720958,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react","react-bootstrap","react-lazy","react-memo","react-performance","reactjs","webpack-bundle-analyzer"],"created_at":"2024-11-21T00:54:56.805Z","updated_at":"2025-07-13T15:34:15.307Z","avatar_url":"https://github.com/icyJoseph.png","language":"JavaScript","readme":"# Testing React Performance\n\nThis repo exists to demonstrate how coding style, available tooling and taking time to examine when our application should change, all have great performance impact on a React Application.\n\n## About the Application\n\nThis app allows you to log yourself as a visitor, by adding your full name and a message to an ever growing list of visitors.\n\nMost recent messages are shown at the top.\n\nEach visitor and its message are represented by an object with the following shape:\n\n```json\n{\n  \"id\": \"32-bit-guid\",\n  \"fullName\": \"Jane Doe\",\n  \"message\": \"The quick brown fox jumps over the lazy dog.\",\n  \"visitDate\": \"2018-12-18\"\n}\n```\n\nA mock server delivers all visitors in an array, already sorted by date, from most recent to oldest.\n\nTo measure performance we use `http://localhost:3000/?react_perf`\n\nTo start the application:\n\n```\nyarn install \u0026\u0026 yarn start-all\n```\n\n## Available Branches\n\n- bad-performance-0\n- bad-performance-1\n- memo-only\n- pure-components\n- should-component-update\n- lazy-loading\n- master\n\nAdditionally we also analyze a production build.\n\n## `bad-performance-0`\n\nFirst naive implementation.\n\n```jsx\nclass App extends Component {\n  state = { visitors: [] };\n  async componentDidMount() {\n    const visitors = await axios\n      .get(\"http://localhost:9191/\")\n      .then(({ data }) =\u003e data);\n    await this.setStateAsync({ visitors });\n  }\n\n  setStateAsync(state) {\n    return new Promise(resolve =\u003e {\n      this.setState(state, resolve);\n    });\n  }\n\n  render() {\n    \u003cdiv\u003e\n      \u003cdiv className=\"App\"\u003e\n        \u003cHeader /\u003e\n      \u003c/div\u003e\n      \u003cdiv className=\"container\"\u003e\n        \u003cdiv className=\"row\"\u003e\n          \u003cdiv className=\"col-lg-6\"\u003e\n            \u003cForm /\u003e\n          \u003c/div\u003e\n          \u003cdiv className=\"col-lg-6\"\u003e\n            \u003cVisitors visitors={visitors} /\u003e\n          \u003c/div\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e;\n  }\n}\n```\n\nOne thing to notice right away is the use of `async` life cycle methods. This allows you to use await inside the life cycle, also consider that `this.setState` behaves asynchronously.\n\nRemember that `this.setState`, takes 2 arguments. The first argument is the new state, or keys to update. The second is a callback, to execute once the update has finished!\n\nSo what's inside `\u003cVisitors/\u003e`?\n\n```jsx\nimport React from \"react\";\n\nexport function Visitors({ visitors }) {\n  return (\n    \u003cul className=\"list-group\"\u003e\n      {visitors.map((visitor, index) =\u003e (\n        \u003cli\n          key={index}\n          className=\"list-group-item d-flex-column justify-content-between align-items-center\"\n        \u003e\n          \u003cdiv className=\"lead\"\u003e{visitor.fullName}\u003c/div\u003e\n          \u003cdiv className=\"text-secondary light-text\"\u003e{visitor.message}\u003c/div\u003e\n          \u003cdiv className=\"text-muted light-text\"\u003e\n            \u003cem\u003e{visitor.visitDate}\u003c/em\u003e\n          \u003c/div\u003e\n        \u003c/li\u003e\n      ))}\n    \u003c/ul\u003e\n  );\n}\n\nexport default Visitors;\n```\n\nWe simply map over the visitors array, rendering a new list element every time. If we don't use the `key` prop, React will give us a warning, so we use the `index` of argument of the `map` function.\n\nThis will work, but creates a problem.\n\nLets look at the Form handler. We want every new message to be at the top, and we want to avoid having to force the client-side code to sort them out again, the back end should have done this for us.\n\n```js\naddNewMessage = async e =\u003e {\n  e.preventDefault();\n\n  const { visitors } = this.state;\n  const fullName = this.fullName.current.value;\n  const message = this.message.current.value;\n  if (fullName \u0026\u0026 message) {\n    // here have an issue -\u003e we do not pass a unique ID to the new visitor element\n    // furthermore we put the new visitor at the beginning!\n    const visitDate = new Date().toISOString().split(\"T\")[0];\n    const updatedVisitors = [{ fullName, message, visitDate }].concat(visitors);\n    await this.setStateAsync({ visitors: updatedVisitors });\n\n    // clear the fields\n    this.fullName.current.value = \"\";\n    this.message.current.value = \"\";\n  }\n  return null;\n};\n```\n\nWe use React Refs to collect the input data for `fullName` and `message`, then if these are not empty, we proceed to generate a day and place our new visitor as first element of visitors, `updatedVisitors`.\n\nThen we set the state, and once done, we clear the inputs.\n\nAlways return null.\n\nBack in `Visitors.js`, something very bad has happened, every element on visitors, has been pushed one index to the right and a new one appended to the beginning, which means the key prop points at a different visitor, now, everytime, 100 + 1 times.\n\nReact has to render every element again, because the Virtual DOM has totally changed!\n\n\u003e The Performance tools in Chrome, show rendering times between `70 to 90 ms`.\n\n![Bad Performance 0 Benchmark][bad-performance-0]\n\n## `bad-performance-1`\n\nA great improvement to `bad-performance-0` would have been to make use of the id's sent by the back end.\n\nSo let's do that! We just need a way to generate a new id for each new message. The back end should do it, but in this setup it does not.\n\nWe can use timestamps or an npm package. I use [uuid.](https://www.npmjs.com/package/uuid)\n\n```js\nimport uuid from \"uuid/v1\";\n\nconst addNewMessage = async e =\u003e {\n  e.preventDefault();\n\n  const { visitors } = this.state;\n  const fullName = this.fullName.current.value;\n  const message = this.message.current.value;\n  if (fullName \u0026\u0026 message) {\n    const id = uuid();\n    const visitDate = new Date().toISOString().split(\"T\")[0];\n    const updatedVisitors = [{ id, fullName, message, visitDate }].concat(\n      visitors\n    );\n    await this.setStateAsync({ visitors: updatedVisitors });\n\n    // clear the fields\n    this.fullName.current.value = \"\";\n    this.message.current.value = \"\";\n  }\n  return null;\n};\n```\n\nSo now back in `Visitors.js`:\n\n```jsx\nimport React from \"react\";\n\nexport function Visitors({ visitors }) {\n  return (\n    \u003cul className=\"list-group\"\u003e\n      {visitors.map(({ id, fullName, message, visitDate }) =\u003e (\n        \u003cli\n          key={id}\n          className=\"list-group-item d-flex-column justify-content-between align-items-center\"\n        \u003e\n          \u003cdiv className=\"lead\"\u003e{fullName}\u003c/div\u003e\n          \u003cdiv className=\"text-secondary light-text\"\u003e{message}\u003c/div\u003e\n          \u003cdiv className=\"text-muted light-text\"\u003e\n            \u003cem\u003e{visitDate}\u003c/em\u003e\n          \u003c/div\u003e\n        \u003c/li\u003e\n      ))}\n    \u003c/ul\u003e\n  );\n}\n\nexport default Visitors;\n```\n\nMuch better! React's Virtual DOM now has better knowledge of each visitor and therefore can avoid re rendering to them DOM. The browser will just handle the addition of a new child.\n\n\u003e The Performance tools in Chrome, show rendering times between `50 to 70 ms`.\n\n![Bad Performance 1 Benchmark][bad-performance-1]\n\nStill high. Since this is a static list, without animations or anything else relying in order, we don't gain so much from using index correctly, but surely there are cases where one can gain a lot.\n\n## `memo-only`\n\nWe notice that the `\u003cli\u003e` element inside Visitors could be extracted away into it's own file, allowing us to read it as a Stateless Functional Component own its own. Which can be optimized further by React.\n\n```jsx\nimport React from \"react\";\nimport Visitor from \"./Visitor\";\n\nexport function Visitors({ visitors }) {\n  return (\n    \u003cul className=\"list-group\"\u003e\n      {visitors.map(({ id, ...visitor }) =\u003e (\n        \u003cVisitor key={id} {...visitor} /\u003e\n      ))}\n    \u003c/ul\u003e\n  );\n}\n```\n\nWhere `\u003cVisitor/\u003e` is:\n\n```jsx\nimport React from \"react\";\n\nfunction Visitor({ fullName, message, visitDate }) {\n  return (\n    \u003cli className=\"list-group-item d-flex-column justify-content-between align-items-center\"\u003e\n      \u003cdiv className=\"lead\"\u003e{fullName}\u003c/div\u003e\n      \u003cdiv className=\"text-secondary light-text\"\u003e{message}\u003c/div\u003e\n      \u003cdiv className=\"text-muted light-text\"\u003e\n        \u003cem\u003e{visitDate}\u003c/em\u003e\n      \u003c/div\u003e\n    \u003c/li\u003e\n  );\n}\n\nexport default React.memo(Visitor);\n```\n\n\u003e Notice the `export default React.memo(Visitor)`\n\nSince, the Visitor React Element has very shallow props, we can make it into a function and memoize it. This means, avoid recalculating the what to render, given the same inputs.\n\nThere's a couple of ways to do this:\n\n- using `function` and `React.memo`, as we've just done with Visitor\n- or using `PureComponent` [here](#pure-components)\n- with `shouldComponentUpdate` life cycle [here](#should-component-update)\n\nAside from creating very beautiful and more readable code, we actually gain a huge performance boost.\n\n\u003e The Performance tools in Chrome, show great improvements by using Memo.\n\n![Memo Only][memo-only]\n\n## \u003ca id=\"pure-components\"\u003e\u003c/a\u003e`pure-components`\n\n```jsx\nimport React, { PureComponent } from \"react\";\n\n// shallow comparisson of props,\n// {id: 1, name: 2} !== {id:2, name:3}\n// but fails to do {id: 1, name:2, dates: [{...}]}\nclass Visitor extends PureComponent {\n  render() {\n    const { fullName, message, visitDate } = this.props;\n    return (\n      \u003cli className=\"list-group-item d-flex-column justify-content-between align-items-center\"\u003e\n        \u003cdiv className=\"lead\"\u003e{fullName}\u003c/div\u003e\n        \u003cdiv className=\"text-secondary light-text\"\u003e{message}\u003c/div\u003e\n        \u003cdiv className=\"text-muted light-text\"\u003e\n          \u003cem\u003e{visitDate}\u003c/em\u003e\n        \u003c/div\u003e\n      \u003c/li\u003e\n    );\n  }\n}\n\nexport default Visitor;\n```\n\n`PureComponent` does a shallow comparisson. It also compares immutable objects, to determine if the component should re-render.\n\n\u003e The Performance tools in Chrome indicate similar reconciliation times between Memo and PureComponents.\n\n![PureComponent][pure-components]\n\n## \u003ca id=\"should-component-update\"\u003e\u003c/a\u003e`should-component-update`\n\n```jsx\nimport React from \"react\";\nimport Visitor from \"./Visitor\";\n\nexport function Visitors({ visitors }) {\n  return (\n    \u003cul className=\"list-group\"\u003e\n      {visitors.map(({ id, ...visitor }) =\u003e (\n        \u003cVisitor key={id} {...visitor} visitorId={id} /\u003e\n      ))}\n    \u003c/ul\u003e\n  );\n}\n\nexport default Visitors;\n```\n\nWe use the id to compare quicker!\n\n```jsx\nimport React, { Component } from \"react\";\n\nclass Visitor extends Component {\n  shouldComponentUpdate(nextProps) {\n    if (nextProps.visitorId !== this.props.visitorId) {\n      return true;\n    }\n    return false;\n  }\n\n  render() {\n    const { fullName, message, visitDate } = this.props;\n    return (\n      \u003cli className=\"list-group-item d-flex-column justify-content-between align-items-center\"\u003e\n        \u003cdiv className=\"lead\"\u003e{fullName}\u003c/div\u003e\n        \u003cdiv className=\"text-secondary light-text\"\u003e{message}\u003c/div\u003e\n        \u003cdiv className=\"text-muted light-text\"\u003e\n          \u003cem\u003e{visitDate}\u003c/em\u003e\n        \u003c/div\u003e\n      \u003c/li\u003e\n    );\n  }\n}\n\nexport default Visitor;\n```\n\nThis lifecycle method can be a glass cannon. It allows us to tell React when to let `componentDidUpdate`, or render run, but we must do the check. And the check can be expensive. For that reason we fallback to using the id, to decide whether or not to re-render.\n\n\u003e The React reconciliation time in Chrome Tools is about the same as memo-only and pure components.\n\n![Should Component Update][should-component-update]\n\n## lazy-loading\n\nPerformance is also affected by loading time.\n\nThis application is very lightweight, which makes it easier to analyze what we are serving to the browser.\n\nFirst, we use `webpack-bundle-analyzer`, a tool to create a graphical report of how our application is structured.\n\n[See Report from Analyzer](https://icjoseph.com/performance/report.html)\n\nAs one can expect, the actual application bundle, our JavaScript, is not that big. The whole application weighs about 100kb, and of that we are about 4%.\n\nHowever, we could easily break down the application further, to optimize what React needs to do upon first load.\n\n```jsx\nimport React, { lazy, Suspense, Component } from \"react\";\n\nconst LazyVisitors = lazy(() =\u003e\n  import(/* webpackChunkName: \"lazy-visitors\" */ \"./Visitors\")\n);\n\nfunction SuspenseVisitors({ ...props }) {\n  return (\n    \u003cSuspense fallback={null}\u003e\n      \u003cLazyVisitors {...props} /\u003e\n    \u003c/Suspense\u003e\n  );\n}\n\nconst LazyForm = lazy(() =\u003e\n  import(/* webpackChunkName: \"lazy-form\" */ \"./Form\")\n);\n\nfunction SuspenseForm({ ...props }) {\n  return (\n    \u003cSuspense fallback={null}\u003e\n      \u003cLazyForm {...props} /\u003e\n    \u003c/Suspense\u003e\n  );\n}\n\nconst LazyHeader = lazy(() =\u003e\n  import(/* webpackChunkName: \"lazy-header\" */ \"./Header\")\n);\n\nfunction SuspenseHeader() {\n  return (\n    \u003cSuspense fallback={null}\u003e\n      \u003cLazyHeader /\u003e\n    \u003c/Suspense\u003e\n  );\n}\n```\n\nNow, we can examine our Network requests in Chrome, and see that we serve three small JavaScript chunks, containing each component.\n\nEach component is now roughly a 1000 Bytes. Had this been a more complicated app, perhaps we could have hundreds of Kb's wait for user demand, and not be pushed on first load.\n\n\u003e The Performance tools in Chrome show the reconciliation time for React.\n\n\u003e Although, there was no improvement, we can see, the `Suspense Update`, which doesn't make things slower!\n\n![Lazy Loading][lazy-loading]\n\n## BUILD!\n\nReact, on development mode, serves with tons of add-ons to make our life easier.\n\nHowever, users do not need these. By creating a production build, we can strip them out.\n\nWe'll lose the ability to pass `?react_perf` to the URL, and any `prop-types` we might be using, but the end user doesn't care.\n\nThe `create-react-app` does this by default, when running `yarn build`.\n\n\u003e In the Chrome dev tools, we can see how long it takes to digest the event.\n\n![Production Build][production-build]\n\n## Inspiration\n\nThis study is inspired by, [this great talk.](https://www.youtube.com/watch?v=nhuwPinAV7E\u0026t=830s)\n\n## License\n\nThe MIT License (MIT)\n\nCopyright (c) 2018 Joseph Chamochumbi\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n[bad-performance-0]: https://icjoseph.com/_static/bad-performance-0.png \"Bad Performance 0\"\n[bad-performance-1]: https://icjoseph.com/_static/bad-performance-1.png \"Bad Performance 1\"\n[memo-only]: https://icjoseph.com/_static/memo-only.png \"Memo Only\"\n[pure-components]: https://icjoseph.com/_static/pure-components.png \"Pure Components\"\n[should-component-update]: https://icjoseph.com/_static/should-component-update.png \"Should Component Update\"\n[lazy-loading]: https://icjoseph.com/_static/lazy-loading.png \"Lazy Loading\"\n[production-build]: https://icjoseph.com/_static/production-build.png \"Production Build\"\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficyjoseph%2Freact-performance-study","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ficyjoseph%2Freact-performance-study","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ficyjoseph%2Freact-performance-study/lists"}