{"id":1447,"date":"2023-10-06T00:00:00","date_gmt":"2023-10-05T22:00:00","guid":{"rendered":"https:\/\/demo.sidekick.be\/itenium\/react-component-profiling\/"},"modified":"2025-11-28T13:31:42","modified_gmt":"2025-11-28T12:31:42","slug":"react-component-profiling","status":"publish","type":"post","link":"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/","title":{"rendered":"React Component Profiling"},"content":{"rendered":"<h1 id=\"react-developer-tools\">React Developer Tools<\/h1>\n<p>The <a href=\"https:\/\/chrome.google.com\/webstore\/detail\/react-developer-tools\/fmkadmapgofadopljbjfkapdkoienihi?hl=en\">React Developer Tools<\/a> Chrome extension adds two tabs to DevTools:<\/p>\n<ul>\n<li>\u269b\ufe0f Components: like Inspect Element but with React tags<\/li>\n<li>\u269b\ufe0f Profiler: the simple and excellent React Component Profiler<\/li>\n<\/ul>\n<h2 id=\"profiler-tab\">Profiler Tab<\/h2>\n<h3 id=\"action-buttons\">Action Buttons<\/h3>\n<ul>\n<li><i class=\"fa fa-circle\" style=\"color: #0086FC\"><\/i> \/ <i class=\"fa fa-circle\" style=\"color: #FC3A4B\"><\/i> : Start \/ Stop profiling<\/li>\n<li><i class=\"fa fa-refresh\" style=\"color: #59616C\"><\/i> : Reload page and start profiling<\/li>\n<li><i class=\"fa fa-ban\" style=\"color: #59616C\"><\/i> : Clear profiling data<\/li>\n<li><i class=\"fa fa-arrow-left\" style=\"color: #59616C\"><\/i> \/ <i class=\"fa fa-arrow-right\" style=\"color: #59616C\"><\/i> : Select previous \/ next commit<\/li>\n<\/ul>\n<h3 id=\"display\">Display<\/h3>\n<ul>\n<li><i class=\"fa fa-fire\" style=\"color: #0086FC\"><\/i> : Flamegraph chart<\/li>\n<li><i class=\"fa fa-sort-amount-desc\" style=\"color: #0086FC\"><\/i> : Ranked chart<\/li>\n<\/ul>\n<h3 id=\"interesting-settings\">Interesting Settings<\/h3>\n<p>Filter the output once it becomes unwieldy in the <i class=\"fa fa-cog\" style=\"color: #59616C\"><\/i> settings modal:<\/p>\n<ul>\n<li><code>Components &gt; Hide components where...<\/code>: custom result filtering based on DOM type (ex: div), name, \u2026<\/li>\n<li><code>Profiler &gt; Hide commits below x ms<\/code>: ignore very fast renders (ex: re-renders for tooltips)<\/li>\n<\/ul>\n<h1 id=\"using-the-profiler\">Using the Profiler<\/h1>\n<p>Press the \u201c<i class=\"fa fa-circle\" style=\"color: #0086FC\"><\/i> Start profiling\u201d button, perform the action(s) that take too long and press the \u201c<i class=\"fa fa-circle\" style=\"color: #FC3A4B\"><\/i> Stop profiling\u201d button. Or use \u201c<i class=\"fa fa-refresh\" style=\"color: #59616C\"><\/i> : Reload page and start profiling\u201d when it\u2019s your initial page load that is taking too long.<\/p>\n<p>The default \u201c<i class=\"fa fa-fire\" style=\"color: #0086FC\"><\/i> : Flamegraph chart\u201d display is perfect as long as the next performance issues are easy to pick out.<\/p>\n<figure class=\"imagewrapper\" style=\"max-width: 650px\"><img decoding=\"async\" src=\"https:\/\/itenium-be.github.io\/blog-posts-new\/assets\/blog-images\/rcp-profiler-flamegraph.png\" alt=\"\" title=\"\" class=\"img-responsive\" \/><figcaption>So what is this &#8216;InvoiceWorkedDays&#8217; component?<\/figcaption><\/figure>\n<h1 id=\"easy-pickings\">Easy Pickings<\/h1>\n<p>\u23f1\ufe0f 6 seconds for 100 invoices\/rows.<\/p>\n<p>The first three profiling rounds revealed that the biggest issues were in non-component code:<\/p>\n<ul>\n<li><strong>Third Party Packages<\/strong>: Do not assume that free, open-source, packages will be performant.<\/li>\n<li><strong>Nested Loops<\/strong>: Several inner loops caused a significant CPU-bound delay.<\/li>\n<li><strong>Frequent I\/O<\/strong>: <code>sessionStorage<\/code> was being accessed for each component wrapped in a role check.<\/li>\n<\/ul>\n<p><\/p>\n<h2 id=\"culprit-1-third-party-package\">Culprit 1: Third Party Package<\/h2>\n<p><code>InvoiceWorkedDays<\/code>, the component in confac that was causing all this fuzz:<\/p>\n<figure class=\"imagewrapper\" style=\"max-width: 650px\"><img decoding=\"async\" src=\"https:\/\/itenium-be.github.io\/blog-posts-new\/assets\/blog-images\/rcp-confac-invoice-list.png\" alt=\"\" title=\"\" class=\"img-responsive\" \/><figcaption>An entire second spent on displaying an amount of days<\/figcaption><\/figure>\n<p>The part that was eating up all this time was <a href=\"https:\/\/github.com\/commenthol\/date-holidays\"><code>date-holidays<\/code><\/a>, an npm package used to calculate the official Belgian holidays.<\/p>\n<p>The date-holidays can calculate holidays for Chinese, Korean and every other calendar out in existence. Because of the un-tree-shakable initialization <code>const hd = new Holidays('BE');<\/code> all the code for those calendars were also being parsed. Sub-dependencies included <a href=\"https:\/\/github.com\/commenthol\/astronomia\"><code>astronomia<\/code><\/a>, leading to a footprint of about 10MB.<\/p>\n<p>We have since replaced the dependency with our own, dedicated (76 liner), npm package <a href=\"https:\/\/github.com\/itenium-be\/date-holidays-be\"><code>@itenium\/date-holidays-be<\/code><\/a>.<\/p>\n<p><\/p>\n<h2 id=\"culprit-2-nested-loops-cpu\">Culprit 2: Nested Loops (CPU)<\/h2>\n<figure class=\"imagewrapper\" style=\"max-width: 650px\"><img decoding=\"async\" src=\"https:\/\/itenium-be.github.io\/blog-posts-new\/assets\/blog-images\/rcp-profiler-flamegraph-InvoiceDownloadIcon.png\" alt=\"\" title=\"\" class=\"img-responsive\" \/><figcaption>Another unexpected performance optimalization target: the InvoiceDownloadIcon<\/figcaption><\/figure>\n<pre><code class=\"language-ts\">\/\/ InvoiceDownloadIcon\nconst fullProjectMonth = useProjectMonthFromInvoice(invoice._id);\n<\/code><\/pre>\n<p>This custom Hook would first make a <code>FullProjectMonth<\/code> of all \u2018projectMonth\u2019 records in the Redux store and then pick the one with <code>invoice._id<\/code>.<\/p>\n<p>Problem here was, that to resolve a \u201cprojectMonth\u201d, 5 other collections needed to be traversed as well. As the data grew, time spent here grew considerably.<\/p>\n<pre><code class=\"language-ts\">\/\/ fn mapToProjectMonth\nconst project = confacState.projects.find(p =&gt; p._id === projectMonth.projectId);\nconst consultant = confacState.consultants.find(c =&gt; c._id === project.consultantId);\nconst client = confacState.clients.find(c =&gt; c._id === project.client.clientId);\nconst partner = confacState.clients.find(c =&gt; project.partner &amp;&amp; c._id === project.partner.clientId);\nconst invoice = confacState.invoices.find(i =&gt; i.projectMonth &amp;&amp; i.projectMonth.projectMonthId === projectMonth._id);\n<\/code><\/pre>\n<p>We can greatly optimize this by first filtering on <code>invoice._id<\/code> so the 5 inner loops are executed just once!<\/p>\n<p><\/p>\n<h2 id=\"culprit-3-io-sessionstorage\">Culprit 3: I\/O (sessionStorage)<\/h2>\n<p>\u23f1\ufe0f 2 seconds for 100 invoices\/rows.<\/p>\n<figure class=\"imagewrapper\" style=\"max-width: 628px\"><img decoding=\"async\" src=\"https:\/\/itenium-be.github.io\/blog-posts-new\/assets\/blog-images\/rcp-profiler-flamegraph-WithClaim.png\" alt=\"\" title=\"\" class=\"img-responsive\" \/><figcaption>And now the WithClaim decorator??? What is happening there&#8230;<\/figcaption><\/figure>\n<p>It\u2019s starting to become difficult to discern the next performance culprit. The second profiler display method \u201c<i class=\"fa fa-sort-amount-desc\" style=\"color: #0086FC\"><\/i> : Ranked chart\u201d to the rescue!<\/p>\n<figure class=\"imagewrapper\" style=\"max-width: 650px\"><img decoding=\"async\" src=\"https:\/\/itenium-be.github.io\/blog-posts-new\/assets\/blog-images\/rcp-profiler-WithClaim-ranked-graph.png\" alt=\"\" title=\"\" class=\"img-responsive\" \/><figcaption>Profiler Ranked chart display<\/figcaption><\/figure>\n<h4 id=\"the-code\">The Code<\/h4>\n<pre><code class=\"language-ts\">\/\/ The decorator\nexport const EnhanceWithClaim = &lt;P extends object&gt;(Component: React.ComponentType&lt;P&gt;) =&gt;\n  class WithClaim extends React.Component&lt;P &amp; EnhanceWithClaimProps&gt; {\n    render() {\n      const {claim, ...props} = this.props;\n      if (claim) {\n        const claims: string[] = authService.getClaims();\n        if (!claims.includes(claim)) {\n          return null;\n        }\n      }\n      return &lt;Component {...props as P} \/&gt;;\n    }\n  };\n\n\/\/ Wrap a component\nconst LinkWithClaim = EnhanceWithClaim(Link);\n\n\/\/ Usage\nconst CreateInvoiceLink = () =&gt; (\n  &lt;LinkWithClaim claim=\"invoice-create\" to=\"\/invoices\/create\"&gt;\n    Create Invoice\n  &lt;\/LinkWithClaim&gt;\n)\n<\/code><\/pre>\n<p>The culprit here turned out to be <code>authService.getClaims()<\/code>, which ended up doing the following for every of the hundreds of icons, buttons etc displayed.<\/p>\n<pre><code class=\"language-ts\">\/\/ I\/O being executed many, many times == performance impact!\nconst token = sessionStorage.getItem('jwt');\n<\/code><\/pre>\n<p><\/p>\n<h2 id=\"alternatives\">Alternatives<\/h2>\n<p>When you are rendering a grid with thousands of rows, you\u2019ll always going to end up with a performance problem (at some point).<\/p>\n<p>There are several mitigations:<\/p>\n<ul>\n<li><strong>Pagination<\/strong><i class=\"fa fa-check-circle\" style=\"color: #AFE1AF\"><\/i> Implemented!<\/li>\n<li><strong>Partial data load<\/strong><small>(only load last year worth of data in memory at startup)<\/small><i class=\"fa fa-check-circle\" style=\"color: #AFE1AF\"><\/i> Implemented!<\/li>\n<li><strong>Debouncing<\/strong>: Useful when filtering the grids, only start a re-render when the user is done entering the filter criteria<\/li>\n<li><strong>Virtualization &amp; Infinite Scroll<\/strong>: A grid can display an \u201cinfinite\u201d amount of rows and still be performant!<\/li>\n<li><strong>Suspense &amp; useDeferredValue<\/strong>: Not relevant here because all data is already in memory<\/li>\n<\/ul>\n<h2 id=\"conclusions\">Conclusions<\/h2>\n<p>\u23f1\ufe0f 360 ms for 100 invoices\/rows.<\/p>\n<p>These were the \u201ceasy pickings\u201d, where the profiler\u2019s flamegraph gave it to us straight. This greatly improved performance (6s -&gt; 360ms) which is good enough.<\/p>\n<ul>\n<li>Not doing a calculation or render is the best optimalization<\/li>\n<li>Keep an eye on 3rd party packages<\/li>\n<li>I\/O operations are expensive<\/li>\n<\/ul>\n<p><\/p>\n<h1 id=\"the-meaty-part\">The Meaty Part<\/h1>\n<p>There is another screen and performance there is\u2026 disastrous.<\/p>\n<ul>\n<li>Everything is re-rendered whenever a projectMonth gets collapsed\/opened.\n<ul>\n<li>Opening more and more projectMonths gets slower and slower.<\/li>\n<li>\u23f1\ufe0f 2 open projectMonths: 350ms<\/li>\n<li>\u23f1\ufe0f 4 open projectMonths: 1.2s<\/li>\n<li>\u23f1\ufe0f 18 open projectMonths: 5s<\/li>\n<\/ul>\n<\/li>\n<li>Filters are remembered between route changes.\n<ul>\n<li>Navigating back can be painfully slow when many projectMonths were open.<\/li>\n<li>\u23f1\ufe0f With 18 open projectMonths: 4.4s<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<figure class=\"imagewrapper\" style=\"max-width: 650px\"><img decoding=\"async\" src=\"https:\/\/itenium-be.github.io\/blog-posts-new\/assets\/blog-images\/rcp-confac-projectMonths.png\" alt=\"\" title=\"\" class=\"img-responsive\" \/><figcaption>The projectMonths screen &#8212; open up more months and you can go get a coffee in between renders \ud83d\ude22<\/figcaption><\/figure>\n<p><\/p>\n<h2 id=\"belgian-holidays-cache\">Belgian Holidays Cache<\/h2>\n<p>For this workshop, <code>date-holidays<\/code> was not replaced with a more dedicated and efficient alternative. Instead we\u2019re using it as a placeholder for something that would actually take a long time and cannot be so easily improved.<\/p>\n<p>To get to the following flamegraph, we added a simple cache for the <code>date-holidays<\/code> dependency.<\/p>\n<pre><code class=\"language-ts\">const workDaysInMonthCache: {[month: string]: number} = {};\n<\/code><\/pre>\n<figure class=\"imagewrapper\" style=\"max-width: 650px\"><img decoding=\"async\" src=\"https:\/\/itenium-be.github.io\/blog-posts-new\/assets\/blog-images\/rcp-profiler-flamegraph-projectMonths.png\" alt=\"\" title=\"\" class=\"img-responsive\" \/><figcaption>A more even distribution now&#8230; So no easy performance targets<\/figcaption><\/figure>\n<h2 id=\"avoiding-ifeature-construction\">Avoiding IFeature Construction<\/h2>\n<p>The <code>IFeature<\/code> configures a grid and CRUD pages for a backend entity.<\/p>\n<p>It contains the data and currently active filters. The problem here is that the <code>openMonths: string[]<\/code> filter is also part of the data of the <code>IFeature<\/code>.<\/p>\n<p>Because of how Redux works, the <code>openMonths: string[]<\/code> filter becomes a new array reference each time a projectMonth is opened\/collapsed. This causes all months to be re-rendered instead of just the one being toggled because the filters object has changed.<\/p>\n<p>Furthermore, the expensive <code>IFeature<\/code> construction is not necessary when the projectMonth is collapsed. Instead we can map directly to <code>CollapsedProjectMonthBadgeTotals<\/code>, which doesn\u2019t include any inner loops.<\/p>\n<pre><code class=\"language-ts\">export const ProjectMonthsList = ({feature, month}: ProjectMonthsListProps) =&gt; {\n  \/\/ If this isn't a Demeter violation, I don't know what is \ud83d\ude13\n  if (feature.list.filter.state.openMonths.includes(month)) {\n    return (\n      &lt;&gt;\n        &lt;Toolbar feature={feature} \/&gt;\n        &lt;List feature={feature} \/&gt;\n      &lt;\/&gt;\n    );\n  }\n\n  \/\/ When the ProjectMonth is collapsed, we don't\n  \/\/ really need to construct the IFeature...\n  return &lt;ProjectMonthListCollapsed feature={feature} \/&gt;;\n};\n<\/code><\/pre>\n<p><\/p>\n<h2 id=\"introducing-reselect\">Introducing: Reselect<\/h2>\n<p>A library for creating <a href=\"https:\/\/github.com\/reduxjs\/reselect\">memoized \u201cselector\u201d functions<\/a>. Commonly used with Redux and comes with the <a href=\"https:\/\/redux-toolkit.js.org\/\">@reduxjs\/toolkit<\/a>.<\/p>\n<p>Caching the expensive mapping to <code>FullProjectMonthModels<\/code> &amp; <code>CollapsedProjectMonthBadgeTotals<\/code> with <code>createSelector<\/code>:<\/p>\n<pre><code class=\"language-ts\">\/\/ Create the selector\nimport { createSelector } from 'reselect';\n\nexport const createFullProjectMonthsSelector = () =&gt; createSelector(\n  \/\/ Select required store data\n  \/\/ Re-renders happen when these references change\n  (state: ConfacState) =&gt; state.projectsMonth,\n  (state: ConfacState) =&gt; state.projects,\n  (state: ConfacState) =&gt; state.consultants,\n\n  \/\/ Introduce a \"month\" parameter for the selector\n  (_, month: string) =&gt; month,\n\n  \/\/ The actual mapping\n  (projectsMonth, projects, consultants, month) =&gt; {\n    const context = { projectsMonth, projects, consultants }\n    return projectsMonth\n      .filter(x =&gt; isSameMonth(x.month, month))\n      .map(x =&gt; mapToProjectMonth(context, x));\n  },\n\n  \/\/ Optional configuration\n  {\n    memoizeOptions: {\n      equalityCheck: (a, b) =&gt; a === b, \/\/ ex: use deep-equal instead\n      maxSize: 10, \/\/ selector cache\n      \/\/ import { shallowEqual } from 'react-redux';\n      resultEqualityCheck: shallowEqual\n    }\n  }\n);\n<\/code><\/pre>\n<p><\/p>\n<h2 id=\"introducing-usememo\">Introducing: useMemo<\/h2>\n<p>Skip expensive recalculations with <a href=\"https:\/\/react.dev\/reference\/react\/useMemo\"><code>useMemo<\/code><\/a><\/p>\n<pre><code class=\"language-ts\">useMemo(calculateValue, dependencies)\n<\/code><\/pre>\n<p>We now need to cache the <code>createFullProjectMonthsSelector<\/code> mapped data for each month. Because we want to keep a memoized version of each month, we pass an empty array as the dependencies argument to useMemo.<\/p>\n<p>If you need to cache only until the month changes, as in when you are navigating from one detail page to another, the dependencies would become <code>[month]<\/code>.<\/p>\n<pre><code class=\"language-ts\">\/\/ Selector usage\nimport { useMemo } from 'react';\n\nexport const OpenedProjectMonthsList = ({ month }: OpenedProjectMonthsListProps) =&gt; {\n  \/\/ Use the createSelector as defined above\n  \/\/ With empty array as dependencies argument\n  const selectProjectMonths = useMemo(createFullProjectMonthsSelector, []);\n\n  \/\/ Use the memoized selector\n  const projectMonths = useSelector(state =&gt; selectProjectMonths(state, month));\n\n  \/\/ And construct the IFeature\n  return &lt;List feature={projectMonthFeature(projectMonths)} \/&gt;;\n};\n<\/code><\/pre>\n<p><\/p>\n<h2 id=\"introducing-memo\">Introducing: memo<\/h2>\n<p>When a <code>ProjectMonthListCollapsed<\/code> is rendered, its details don\u2019t change, so we can do some aggressive caching there, keeping the entire rendered component in the cache with <a href=\"https:\/\/react.dev\/reference\/react\/memo\"><code>React.memo(component, propsAreEqual?)<\/code><\/a>.<\/p>\n<pre><code class=\"language-ts\">import { memo } from 'react';\n\n\/\/ Memoize the component for each value of month\nconst ProjectMonthListCollapsedMemo = memo(({ month }) =&gt; {\n  return &lt;ProjectMonthListCollapsed month={month} \/&gt;;\n});\n\n\nexport const OpenOrClosedProjectMonthsList = ({ month }) =&gt; {\n  const isOpen = useSelector(state =&gt; state.filters.projectMonths.openMonths[month]);\n  if (isOpen) {\n    return &lt;OpenedProjectMonthsList month={month} \/&gt;;\n  }\n  return &lt;ProjectMonthListCollapsedMemo month={month} \/&gt;;\n};\n<\/code><\/pre>\n<p><\/p>\n<h2 id=\"the-aftermath\">The Aftermath<\/h2>\n<figure class=\"imagewrapper\" style=\"max-width: 650px\"><img decoding=\"async\" src=\"https:\/\/itenium-be.github.io\/blog-posts-new\/assets\/blog-images\/rcp-profiler-projectMonths-final.png\" alt=\"\" title=\"\" class=\"img-responsive\" \/><figcaption>Flamegraph after opening a second projectMonth<\/figcaption><\/figure>\n<ol>\n<li>No re-render of the previously opened projectMonth (reselect &amp; useMemo)<\/li>\n<li>The render of the newly opened projectMonth<\/li>\n<li>The completely cached collapsed\/closed projectMonths (memo)<\/li>\n<\/ol>\n<h2 id=\"conclusions-1\">Conclusions<\/h2>\n<ul>\n<li>\u23f1\ufe0f 2 projectMonths: 350ms -&gt; 170ms<\/li>\n<li>\u23f1\ufe0f 4 projectMonths: 1.2s -&gt; 311ms<\/li>\n<li>\u23f1\ufe0f 18 open projectMonths: 5s -&gt; 140ms<\/li>\n<\/ul>\n<p>The biggest win was avoiding the re-renders of all projectMonths when opening\/closing just one and caching expensive Redux selects.<\/p>\n<h3 id=\"take-aways\">Take-Aways<\/h3>\n<ul>\n<li>Depend on as little as possible\n<ul>\n<li>Components should rely on their minimum requirements of state and props only<\/li>\n<li>Prefer props only for low level components<\/li>\n<\/ul>\n<\/li>\n<li>Cache expensive calculations<\/li>\n<li>createSelector, useMemo and memo are your friends<\/li>\n<\/ul>\n<h4 id=\"the-ifeature\">The IFeature<\/h4>\n<blockquote>\n<p><em>\u201cWeeks of coding can save you hours of planning.\u201d<\/em> \u2014 Fred Brooks<\/p>\n<\/blockquote>\n<h3 id=\"unexpected-side-effect\">Unexpected Side-Effect<\/h3>\n<p>The overall structure and code quality has improved due the refactorings needed to realize the performance targets.<\/p>\n<ul>\n<li>More components with finer grained purpose resulted in a clearer component hierarchy<\/li>\n<li>Complexity has increased but it is hidden away in hooks and selectors<\/li>\n<\/ul>\n<h1 id=\"profiler-alternatives\">Profiler Alternatives<\/h1>\n<p>If the Chrome Profiler Extension isn\u2019t cutting it for you anymore, you can still fallback to these alternatives for locating performance optimalization targets.<\/p>\n<h2 id=\"consoletime\">console.time<\/h2>\n<p>Good old <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/console\/timeEnd\">console<\/a> \ud83d\ude09<\/p>\n<pre><code class=\"language-ts\">console.time('timer1')\n\nconst result = expensiveOp1()\nconsole.timeLog('timer1', 'result is', result)\nexpensiveOp2()\n\nconsole.timeEnd('timer1')\n<\/code><\/pre>\n<h2 id=\"profiler-component\">Profiler Component<\/h2>\n<p><a href=\"https:\/\/react.dev\/reference\/react\/Profiler\"><code>&lt;Profiler \/&gt;<\/code><\/a> doesn\u2019t actually render anything and can be nested by providing different ids.<\/p>\n<pre><code class=\"language-ts\">import { Profiler } from 'react';\n\nfunction callback(\n  id, \/\/ the \"id\" prop of the Profiler tree that has just committed\n  phase, \/\/ either \"mount\" (if the tree just mounted) or \"update\" (if it re-rendered)\n  actualDuration, \/\/ time spent rendering the committed update\n  baseDuration, \/\/ estimated time to render the entire subtree without memoization\n  startTime, \/\/ when React began rendering this update\n  commitTime, \/\/ when React committed this update\n  interactions \/\/ the Set of interactions belonging to this update\n) {\n  console.log(`${id}: ${phase} in ${actualDuration}`);\n}\n\nconst LongRendering = () =&gt; (\n  &lt;Profiler id=\"uniqueId\" onRender={callback}&gt;\n    &lt;LongRenderingItem \/&gt;\n  &lt;\/Profiler&gt;\n)\n<\/code><\/pre>\n<p>For a simple case like this with only one <code>&lt;Profiler<\/code>, you are better off just using the Chrome extension.<\/p>\n<p>It can come in handy for micro optimizations, using multiple <code>&lt;Profiler \/&gt;<\/code>s and hundreds\/thousands of components. At that point you will need to write your own aggregator in the callback to make some sense of the data.<\/p>\n<p>A simple aggregator on <code>ID<\/code>:<\/p>\n<pre><code class=\"language-ts\">type ITiming = {\n  renders: number;\n  totalTime: number;\n  averageTime?: number;\n}\n\nconst timings: {[key: string]: ITiming} = {}\n\nfunction callback(id, phase, actualDuration) {\n  if (!timings[id]) {\n    timings[id] = {renders: 1, totalTime: actualDuration};\n  } else {\n    const oldTimings = timings[id];\n    timings[id] = {\n      renders: oldTimings.renders + 1,\n      totalTime: oldTimings.totalTime + actualDuration\n    };\n  }\n\n  timings[id].averageTime = timings[id].totalTime \/ timings[id].renders;\n}\n<\/code><\/pre>\n<h1 id=\"further-optimalizations\">Further Optimalizations<\/h1>\n<ul>\n<li>Avoid inline styles<\/li>\n<li><a href=\"https:\/\/react.dev\/reference\/react\/useCallback\"><strong>useCallback<\/strong><\/a>: cache event handlers\n<ul>\n<li>Tricky: useCallback also takes time; in my case it actually degraded performance<\/li>\n<li>As always with perf optimizations \u2013&gt; <strong>MEASURE<\/strong> (the profiler is your friend)<\/li>\n<\/ul>\n<\/li>\n<li><strong>memo<\/strong>: Components without props can be cached entirely<\/li>\n<li><a href=\"https:\/\/github.com\/itenium-be\/React-ComponentProfiling\/blob\/master\/snippets\/useWhyDidYouUpdate.ts\"><strong>useWhyDidYouUpdate<\/strong><\/a>: part of many hooks-libraries (I didn\u2019t find it very useful)<\/li>\n<li><a href=\"https:\/\/react.dev\/reference\/react\/useDebugValue\"><strong>useDebugValue<\/strong><\/a>: to be used in custom hooks (more for library developers IMO)<\/li>\n<\/ul>\n<h1>Extras<\/h1>\n<p><b>Stuff that came into being during the making of this post<\/b><\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/itenium-be\/React-ComponentProfiling\">Presentation source materials (pptx, code snippets, &#8230;)<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/itenium-be\/confac\">confac Github source<\/a><\/li>\n<\/ul>\n<p><b>Other interesting reads<\/b><\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/amsterdamharu\/selectors\" class=\"github\"><img decoding=\"async\" src=\"https:\/\/img.shields.io\/github\/stars\/amsterdamharu\/selectors.svg?style=social&#038;label=Star\" \/><\/a><a href=\"https:\/\/github.com\/amsterdamharu\/selectors\">amsterdamharu\/selectors<\/a>: Examples of reselect selectors<\/li>\n<\/ul>\n<h1> Package Versions <i class=\"fa fa-info-circle\" style=\"float: right; color: cornflowerblue; font-size: 28px\" title=\"Package versions used for this post and in the accompanying code on Github\"><\/i><\/h1>\n<div><a href=\"https:\/\/github.com\/facebook\/react\" class=\"github\"><img decoding=\"async\" src=\"https:\/\/img.shields.io\/github\/stars\/facebook\/react.svg?style=social&#038;label=Star\" \/><\/a><a href=\"https:\/\/github.com\/facebook\/react\">facebook\/react<\/a>: v18.2.0 <\/div>\n","protected":false},"excerpt":{"rendered":"<p>Serious rendering performance troubles in <a href=\"https:\/\/github.com\/itenium-be\/confac\">confac<\/a>, our internal React invoicing app. It reads the entire database in memory \ud83d\ude32 and works pretty much exclusively on the frontend with the Redux store, so it\u2019s not the db that is the performance bottleneck for once \ud83d\ude1c<\/p>\n<p>Worst case scenario on the <code>\/monthly-invoicing<\/code> page is a whopping 10s wait time for a single screen to become responsive. Not good.<\/p>\n","protected":false},"author":1,"featured_media":2009,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[3],"tags":[22,10],"class_list":["post-1447","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-javascript","tag-react","tag-war-story"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.5 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>React Component Profiling - Itenium<\/title>\n<meta name=\"robots\" content=\"noindex, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"React Component Profiling - Itenium\" \/>\n<meta property=\"og:description\" content=\"Serious rendering performance troubles in confac, our internal React invoicing app. It reads the entire database in memory \ud83d\ude32 and works pretty much exclusively on the frontend with the Redux store, so it\u2019s not the db that is the performance bottleneck for once \ud83d\ude1cWorst case scenario on the \/monthly-invoicing page is a whopping 10s wait time for a single screen to become responsive. Not good.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/\" \/>\n<meta property=\"og:site_name\" content=\"Itenium\" \/>\n<meta property=\"article:published_time\" content=\"2023-10-05T22:00:00+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-11-28T12:31:42+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/react-profiling.png\" \/>\n\t<meta property=\"og:image:width\" content=\"360\" \/>\n\t<meta property=\"og:image:height\" content=\"300\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Usdevelop\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Usdevelop\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/\",\"url\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/\",\"name\":\"React Component Profiling - Itenium\",\"isPartOf\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/react-profiling.png\",\"datePublished\":\"2023-10-05T22:00:00+00:00\",\"dateModified\":\"2025-11-28T12:31:42+00:00\",\"author\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022\"},\"breadcrumb\":{\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#primaryimage\",\"url\":\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/react-profiling.png\",\"contentUrl\":\"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/react-profiling.png\",\"width\":360,\"height\":300,\"caption\":\"a wristwatch with inside the react logo\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/demo.sidekick.be\/itenium\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"React Component Profiling\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#website\",\"url\":\"https:\/\/demo.sidekick.be\/itenium\/\",\"name\":\"Itenium\",\"description\":\"We build software. We build careers.\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/demo.sidekick.be\/itenium\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022\",\"name\":\"Usdevelop\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g\",\"caption\":\"Usdevelop\"},\"sameAs\":[\"https:\/\/demo.sidekick.be\/itenium\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"React Component Profiling - Itenium","robots":{"index":"noindex","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"og_locale":"en_US","og_type":"article","og_title":"React Component Profiling - Itenium","og_description":"Serious rendering performance troubles in confac, our internal React invoicing app. It reads the entire database in memory \ud83d\ude32 and works pretty much exclusively on the frontend with the Redux store, so it\u2019s not the db that is the performance bottleneck for once \ud83d\ude1cWorst case scenario on the \/monthly-invoicing page is a whopping 10s wait time for a single screen to become responsive. Not good.","og_url":"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/","og_site_name":"Itenium","article_published_time":"2023-10-05T22:00:00+00:00","article_modified_time":"2025-11-28T12:31:42+00:00","og_image":[{"width":360,"height":300,"url":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/react-profiling.png","type":"image\/png"}],"author":"Usdevelop","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Usdevelop","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/","url":"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/","name":"React Component Profiling - Itenium","isPartOf":{"@id":"https:\/\/demo.sidekick.be\/itenium\/#website"},"primaryImageOfPage":{"@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#primaryimage"},"image":{"@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#primaryimage"},"thumbnailUrl":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/react-profiling.png","datePublished":"2023-10-05T22:00:00+00:00","dateModified":"2025-11-28T12:31:42+00:00","author":{"@id":"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022"},"breadcrumb":{"@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#primaryimage","url":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/react-profiling.png","contentUrl":"https:\/\/demo.sidekick.be\/itenium\/wp-content\/uploads\/2025\/11\/react-profiling.png","width":360,"height":300,"caption":"a wristwatch with inside the react logo"},{"@type":"BreadcrumbList","@id":"https:\/\/demo.sidekick.be\/itenium\/blog\/javascript\/react-component-profiling\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/demo.sidekick.be\/itenium\/"},{"@type":"ListItem","position":2,"name":"React Component Profiling"}]},{"@type":"WebSite","@id":"https:\/\/demo.sidekick.be\/itenium\/#website","url":"https:\/\/demo.sidekick.be\/itenium\/","name":"Itenium","description":"We build software. We build careers.","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/demo.sidekick.be\/itenium\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/6ab401ce01e858184ee4a8667fe0f022","name":"Usdevelop","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/demo.sidekick.be\/itenium\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/cc8cf9b9e9d5bbe7e8d247907cc9ac8457bbfa2854b52eaddf6529adfe88ceab?s=96&d=mm&r=g","caption":"Usdevelop"},"sameAs":["https:\/\/demo.sidekick.be\/itenium"]}]}},"_links":{"self":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts\/1447","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/comments?post=1447"}],"version-history":[{"count":3,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts\/1447\/revisions"}],"predecessor-version":[{"id":2269,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/posts\/1447\/revisions\/2269"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/media\/2009"}],"wp:attachment":[{"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/media?parent=1447"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/categories?post=1447"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/demo.sidekick.be\/itenium\/wp-json\/wp\/v2\/tags?post=1447"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}