{"id":318,"date":"2026-06-12T05:01:58","date_gmt":"2026-06-12T05:01:58","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/react-compiler-what-i-deleted-after-turning-it-on\/"},"modified":"2026-06-12T05:01:58","modified_gmt":"2026-06-12T05:01:58","slug":"react-compiler-what-i-deleted-after-turning-it-on","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/react-compiler-what-i-deleted-after-turning-it-on\/","title":{"rendered":"React Compiler: What I Deleted After Turning It On"},"content":{"rendered":"<p>I turned on the React Compiler in a real app back in March, fully expecting to lose a weekend to cleanup. It took about forty minutes. I deleted a depressing number of <code>useMemo<\/code> calls, and then sat there feeling a little insulted by how much hand-wired memoization I&rsquo;d been hauling around for years.<\/p>\n<p>Here&rsquo;s the confession. I was one of those people who put <code>useCallback<\/code> on every function I passed to a child component, because a blog post in 2021 told me to. I never measured whether it helped. I just did it, the way you wear a lucky shirt to an interview.<\/p>\n<p>The compiler made most of that pointless. Not all of it. There&rsquo;s a real gap between &ldquo;memoization is handled for you now&rdquo; and &ldquo;you can delete every optimization in the codebase,&rdquo; and I fell into that gap twice before the rules clicked. So here&rsquo;s what actually changed for me, what I deleted, and the two places where the compiler quietly does nothing.<\/p>\n<h2 id=\"what-the-compiler-actually-does\">What the compiler actually does<\/h2>\n<p>The short version: the <a href=\"https:\/\/react.dev\/learn\/react-compiler\" rel=\"nofollow noopener\" target=\"_blank\">React Compiler<\/a> reads your components and hooks at build time, works out what depends on what, and inserts memoization for you. Values that don&rsquo;t need to change between renders keep the same reference. Components stop re-rendering when their inputs are the same as last time. You don&rsquo;t write any of it by hand.<\/p>\n<p>Before, that bookkeeping was your job. You decided what to wrap in <code>useMemo<\/code>, which callbacks needed <code>useCallback<\/code>, and where a <code>React.memo<\/code> boundary earned its keep. Get it wrong in one direction and you re-render too much. Get it wrong in the other and you cache a stale value and chase a bug for an afternoon. The compiler moves that decision into the build step, where it can see the whole component at once instead of relying on you to remember.<\/p>\n<p>It shipped as part of the <a href=\"https:\/\/react.dev\/blog\/2024\/12\/05\/react-19\" rel=\"nofollow noopener\" target=\"_blank\">React 19 release<\/a>, and the thing I underestimated is that it&rsquo;s conservative. If it can&rsquo;t prove a component follows the Rules of React, it skips that component and leaves your code exactly as written. It would rather do nothing than miscompile.<\/p>\n<h2 id=\"the-usememo-and-usecallback-i-deleted\">The useMemo and useCallback I deleted<\/h2>\n<p>This is the part that felt good. Here&rsquo;s a list component the way I&rsquo;d have written it for React 18:<\/p>\n<pre><code class=\"language-jsx\">function ProductList({ products, query }) {\n  const filtered = useMemo(\n    () =&gt; products.filter((p) =&gt; p.name.includes(query)),\n    [products, query]\n  );\n\n  const handleSelect = useCallback(\n    (id) =&gt; analytics.track(&quot;select&quot;, id),\n    []\n  );\n\n  return &lt;List items={filtered} onSelect={handleSelect} \/&gt;;\n}\n<\/code><\/pre>\n<p>Two hooks, two dependency arrays, two chances to get the dependencies wrong. With the compiler on, the same component is just this:<\/p>\n<pre><code class=\"language-jsx\">function ProductList({ products, query }) {\n  const filtered = products.filter((p) =&gt; p.name.includes(query));\n  const handleSelect = (id) =&gt; analytics.track(&quot;select&quot;, id);\n\n  return &lt;List items={filtered} onSelect={handleSelect} \/&gt;;\n}\n<\/code><\/pre>\n<p>The compiler memoizes <code>filtered<\/code> so the filter only runs again when <code>products<\/code> or <code>query<\/code> change. It keeps <code>handleSelect<\/code> stable so <code>&lt;List&gt;<\/code> doesn&rsquo;t re-render just because the parent did. I wrote the obvious code and got the optimized behavior for free, which is roughly the opposite of how the last decade of React felt.<\/p>\n<p>I did the same cleanup I wrote about when I <a href=\"https:\/\/abrarqasim.com\/blog\/react-19-forwardref-why-i-stopped-writing-it\" rel=\"noopener\">stopped writing forwardRef by hand<\/a>: rip out the ceremony, keep the component, confirm nothing regressed in the Profiler. The dependency arrays were the part I was happiest to see go. Most of the memoization bugs I&rsquo;ve shipped came from a dependency array that drifted out of sync with the function inside it.<\/p>\n<p>The quieter win is what it did to code review. We used to argue about whether a given <code>useMemo<\/code> was pulling its weight, and nobody could answer without profiling, so the comment thread just died and the hook stayed. With the compiler doing the bookkeeping, those threads stopped happening. A pull request now shows the actual logic instead of a layer of caching wrapped around it, and a reviewer can read what the component does rather than reverse-engineering why three hooks are there. I didn&rsquo;t expect a build tool to make diffs easier to read, but that&rsquo;s been the change I notice most often, more than any frame-rate number.<\/p>\n<h2 id=\"turning-it-on-without-breaking-everything\">Turning it on without breaking everything<\/h2>\n<p>I didn&rsquo;t flip a switch and pray. The compiler is a Babel plugin, and the safest path is to add it behind the linter first.<\/p>\n<pre><code class=\"language-js\">\/\/ babel.config.js\nmodule.exports = {\n  plugins: [\n    [&quot;babel-plugin-react-compiler&quot;, { target: &quot;19&quot; }],\n  ],\n};\n<\/code><\/pre>\n<p>Before that, I ran the compiler&rsquo;s ESLint rule across the codebase to see how many components broke the Rules of React. That number is your real migration cost. A component that mutates props during render, or reads a ref in the wrong place, is a component the compiler will refuse to touch. The <a href=\"https:\/\/react.dev\/learn\/react-compiler\/installation\" rel=\"nofollow noopener\" target=\"_blank\">installation guide<\/a> walks through the incremental path, and I&rsquo;d push back on anyone who tells you to skip it. Knowing which files the compiler is ignoring is more useful than knowing it&rsquo;s &ldquo;on.&rdquo;<\/p>\n<p>When you find a component that misbehaves under compilation and you can&rsquo;t fix it that minute, you can opt it out with a directive instead of reverting the whole rollout:<\/p>\n<pre><code class=\"language-jsx\">function LegacyChart(props) {\n  &quot;use no memo&quot;;\n  \/\/ ...the gnarly one you'll refactor later\n}\n<\/code><\/pre>\n<p>I had exactly one of those. It was a chart wrapper doing something cursed with a mutable ref, and tagging it bought me time to fix it properly the following week.<\/p>\n<h2 id=\"where-it-still-doesnt-save-you\">Where it still doesn&rsquo;t save you<\/h2>\n<p>Now the part nobody puts in the launch posts. I watched the compiler do nothing useful in two situations, and both times the problem was mine, not the compiler&rsquo;s.<\/p>\n<p>The first is genuinely expensive work whose inputs change every render. Memoization caches a result keyed on its inputs. If a parent hands you a freshly built object on every render, the key is never the same twice, so there&rsquo;s nothing to reuse:<\/p>\n<pre><code class=\"language-jsx\">\/\/ Parent rebuilds `options` every render, so the key is always new.\n&lt;Report options={{ range: &quot;30d&quot;, tz: userTz }} data={rows} \/&gt;\n<\/code><\/pre>\n<p>The compiler can&rsquo;t memoize across renders when the input is never stable. The fix lives upstream: build that object once, or lift it out, or pass primitives. No build step rescues a render that throws away its own inputs.<\/p>\n<p>The second is effects firing too often because of a dependency created outside React&rsquo;s view. The compiler reasons about component and hook scope. A value living in module scope or a non-React store isn&rsquo;t something it tracks, so an effect that depends on it keeps re-running. That&rsquo;s usually a data-flow problem, and these days I reach for a proper query layer instead, which I got into when I wrote about <a href=\"https:\/\/abrarqasim.com\/blog\/tanstack-query-2026-what-i-reach-for-instead-of-useeffect\" rel=\"noopener\">what I use instead of useEffect for fetching<\/a>. The compiler optimizes rendering. It doesn&rsquo;t reorganize your architecture, and it&rsquo;s honest enough not to pretend otherwise.<\/p>\n<h2 id=\"so-should-you-turn-it-on-this-week\">So should you turn it on this week?<\/h2>\n<p>Yes, but in a branch, and with the Profiler open. Here&rsquo;s the order I&rsquo;d run it in. Add the ESLint rule and read the violations before you change a single line, because that list is the actual scope of the work. Turn the compiler on in a build and confirm the app still behaves. Profile one real interaction, the slow one you already know about, and compare before and after. Only then start deleting <code>useMemo<\/code> and <code>useCallback<\/code>, and delete them in small batches so a regression is easy to trace.<\/p>\n<p>What I&rsquo;d avoid is the bulk find-and-replace that strips every memoization hook in one commit. The compiler is good, but a few of those hooks were load-bearing for reasons you&rsquo;ve forgotten, and you want to find that out one file at a time. I keep a running list of these &ldquo;things I finally deleted&rdquo; write-ups in my <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">work<\/a>, and this one was the most satisfying entry in a while, mostly because the cleanup was real and the risk was low.<\/p>\n<p>If you&rsquo;ve got an afternoon, start with the ESLint rule. It tells you more about your codebase than the compiler itself does.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I switched on the React Compiler in a real app and deleted most of my useMemo and useCallback calls. Here&#8217;s what changed and where it still does nothing.<\/p>\n","protected":false},"author":2,"featured_media":317,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I switched on the React Compiler in a real app and deleted most of my useMemo and useCallback calls. Here's what changed and where it still does nothing.","rank_math_focus_keyword":"react compiler","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[354],"tags":[38,44,19,41,359,358],"class_list":["post-318","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-react","tag-frontend","tag-javascript","tag-performance","tag-react","tag-react-19-2","tag-react-compiler-2"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/318","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=318"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/318\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/317"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=318"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=318"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=318"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}