Measuring Before Optimizing

The cheapest instrument you own is a counter in a component body and performance.now() around a render. flushSync makes the timing honest: it forces React 7,897 to finish the render and the commit before it returns, so the number covers the whole pass, not the call that scheduled it.

Timing a mount and an update, and counting rendersHTML
<div id="root"></div><p id="out" style="font:14px system-ui"></p>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@8.0.5/babel.min.js"></script>
<script type="importmap">{ "imports": { "react": "https://esm.sh/react@19.3.0",
  "react-dom": "https://esm.sh/react-dom@19.3.0",
  "react-dom/client": "https://esm.sh/react-dom@19.3.0/client" } }</script>
<script type="text/babel" data-type="module">
  import React from 'react';
  import { createRoot } from 'react-dom/client'; import { flushSync } from 'react-dom';
  let renders = 0;                                   // the cheapest instrument there is
  const Row = ({ n }) => { renders++;
    let s = 0; for (let k = 0; k < 2e4; k++) s += Math.sqrt(k + n);
    return <li>row {n}: {s.toFixed(0)}</li>; };
  const List = ({ q }) => <ul style={{ height: 22, overflow: 'hidden', margin: 0 }}>
    {Array.from({ length: 500 }, (_, i) => <Row key={i} n={i + q} />)}</ul>;
  const root = createRoot(document.getElementById('root'));
  const time = (el) => { const t = performance.now(); flushSync(() => root.render(el));
    return (performance.now() - t).toFixed(1); };
  const mount = time(<List q={0} />), update = time(<List q={1} />);
  out.innerHTML = `mount <b>${mount} ms</b>, update <b>${update} ms</b>, ` +
    `${renders} Row renders`;
</script>
Browser output of Listing 2.96
Browser output of 96

Three facts fall out of one screenshot. The update is cheaper than the mount because React reuses the existing DOM nodes; all 500 rows re-rendered on both passes; and an update of this size has already blown past the 16.7 ms a 60 Hz frame allows. A counter like renders answers the question memoization turns on — is this component re-rendering more often than its inputs change? — in any build.

For the load story the instrument is the browser: Lighthouse 30,824 for Largest Contentful Paint, the Performance panel for Interaction to Next Paint, React DevTools 7,897 ' Profiler tab for ranking components by time in a commit. Run all three against a production build — a development build exaggerates render cost and hides bundle wins.