{"id":207,"date":"2026-05-10T05:01:48","date_gmt":"2026-05-10T05:01:48","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/react-compiler-2026-what-i-removed-when-i-turned-it-on\/"},"modified":"2026-05-10T05:01:48","modified_gmt":"2026-05-10T05:01:48","slug":"react-compiler-2026-what-i-removed-when-i-turned-it-on","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/react-compiler-2026-what-i-removed-when-i-turned-it-on\/","title":{"rendered":"React Compiler in 2026: What I Removed When I Turned It On"},"content":{"rendered":"<p>I put off turning on the React Compiler for almost a year. Not because I doubted it would work, but because every time I read the docs I&rsquo;d think &ldquo;okay, but my app already memoizes the right things, what would actually change?&rdquo; and close the tab.<\/p>\n<p>Then I had a Sunday with nothing to do, so I flipped it on in a real Next.js app I&rsquo;d been babysitting for about eighteen months. The compiler is stable in <a href=\"https:\/\/react.dev\/learn\/react-compiler\" rel=\"nofollow noopener\" target=\"_blank\">React 19<\/a>, the eslint plugin had calmed down, and the worst case was reverting one PR.<\/p>\n<p>What I found is that I&rsquo;d been writing a lot of memoization that the compiler now does for me, and a few patterns I genuinely hadn&rsquo;t realized were broken. Here&rsquo;s a tour of what I deleted, what I kept, and the rough edges that aren&rsquo;t in the announcement posts.<\/p>\n<h2 id=\"what-the-compiler-actually-does\">What the compiler actually does<\/h2>\n<p>The marketing line is &ldquo;automatic memoization.&rdquo; That&rsquo;s accurate but underselling. What it actually does is read your component, work out which values derive from which inputs, and emit a version that only recomputes things when the inputs change. Effectively, it inserts the <code>useMemo<\/code> and <code>useCallback<\/code> calls a careful person would insert, and inserts them in places a careful person wouldn&rsquo;t bother.<\/p>\n<p>If you&rsquo;ve read the <a href=\"https:\/\/react.dev\/reference\/react-compiler\" rel=\"nofollow noopener\" target=\"_blank\">React Compiler reference<\/a>, you&rsquo;ve seen the example with a <code>&lt;TodoList&gt;<\/code> and a sort callback that the compiler memoizes for you. In a real app the wins are subtler. You stop seeing prop changes that don&rsquo;t matter trigger child re-renders, and the diff between commits gets smaller. I noticed it most in a few list components where I&rsquo;d given up on memoization because every parent change blew through it anyway.<\/p>\n<p>It runs at build time. There&rsquo;s no runtime cost beyond the slightly larger output, and the output is plain React: hooks, components, no special runtime. That&rsquo;s the part I trusted most before turning it on.<\/p>\n<h2 id=\"the-usememo-and-usecallback-i-deleted\">The useMemo and useCallback I deleted<\/h2>\n<p>I went through the codebase the day after enabling the compiler and pulled hand-rolled memoization that no longer earned its keep. Here&rsquo;s a representative chunk before:<\/p>\n<pre><code class=\"language-jsx\">function ProjectsTable({ projects, query, sortKey }) {\n  const filtered = useMemo(\n    () =&gt; projects.filter(p =&gt; p.name.toLowerCase().includes(query.toLowerCase())),\n    [projects, query]\n  );\n\n  const sorted = useMemo(\n    () =&gt; [...filtered].sort((a, b) =&gt; a[sortKey].localeCompare(b[sortKey])),\n    [filtered, sortKey]\n  );\n\n  const handleRowClick = useCallback(\n    (id) =&gt; router.push(`\/projects\/${id}`),\n    [router]\n  );\n\n  return &lt;Table rows={sorted} onRowClick={handleRowClick} \/&gt;;\n}\n<\/code><\/pre>\n<p>After:<\/p>\n<pre><code class=\"language-jsx\">function ProjectsTable({ projects, query, sortKey }) {\n  const filtered = projects.filter(p =&gt;\n    p.name.toLowerCase().includes(query.toLowerCase())\n  );\n  const sorted = [...filtered].sort((a, b) =&gt;\n    a[sortKey].localeCompare(b[sortKey])\n  );\n  const handleRowClick = (id) =&gt; router.push(`\/projects\/${id}`);\n\n  return &lt;Table rows={sorted} onRowClick={handleRowClick} \/&gt;;\n}\n<\/code><\/pre>\n<p>That&rsquo;s the whole point. The compiler sees that <code>filtered<\/code> only depends on <code>projects<\/code> and <code>query<\/code>, and that <code>sorted<\/code> only depends on <code>filtered<\/code> and <code>sortKey<\/code>, and emits cached versions. The function identity for <code>handleRowClick<\/code> is now stable across renders unless <code>router<\/code> actually changes.<\/p>\n<p>Across the project I removed about forty <code>useMemo<\/code> and <code>useCallback<\/code> calls. Maybe twelve of them were doing real work; the rest were premature, copy-paste guards I&rsquo;d added because some prop was getting passed to a memoized child and I didn&rsquo;t want to think about it. The diff was satisfying in a small petty way.<\/p>\n<h2 id=\"the-places-it-didnt-help\">The places it didn&rsquo;t help<\/h2>\n<p>Two things bit me, and neither is in the headline announcement.<\/p>\n<p>The first is referential equality across hooks I don&rsquo;t own. If a third-party hook returns a fresh object every render, no compiler in the world can save you. I had a date-range hook that returned <code>{ from, to }<\/code> as a new literal each call. The compiler dutifully memoized everything downstream of it, but the inputs themselves changed every render, so nothing was actually cached. The fix was to wrap the hook (or, in my case, replace it with a tiny one that returned a stable reference). Worth checking your dependencies before assuming the compiler magically fixed things.<\/p>\n<pre><code class=\"language-jsx\">\/\/ The third-party hook (out of my control)\nfunction useDateRange() {\n  \/\/ returns a new object literal every render\n  return { from: startOfWeek(new Date()), to: new Date() };\n}\n\n\/\/ My wrapper that gives me a stable reference\nfunction useStableDateRange() {\n  const { from, to } = useDateRange();\n  return useMemo(\n    () =&gt; ({ from, to }),\n    [from.getTime(), to.getTime()]\n  );\n}\n<\/code><\/pre>\n<p>The second is <code>useEffect<\/code> dependencies. The compiler doesn&rsquo;t rewrite your effects&rsquo; dependency arrays. If you had a nasty effect that re-ran because <code>onChange<\/code> was a new function each render, that&rsquo;s still a problem, except now <code>onChange<\/code> is stable, so the bug presents differently. I had one effect that used to fire constantly, then started firing once per real change after I enabled the compiler, and that exposed a race I hadn&rsquo;t noticed before. Net positive, but worth knowing.<\/p>\n<p>I poked at a few of these interactions earlier this year in <a href=\"https:\/\/abrarqasim.com\/blog\/react-19-features-i-actually-use-six-months-in\" rel=\"noopener\">the React 19 features I actually use six months in<\/a>, if you want a related read.<\/p>\n<h2 id=\"the-eslint-plugin-is-the-real-shipping-criterion\">The eslint plugin is the real shipping criterion<\/h2>\n<p>Here&rsquo;s what nobody told me clearly: the compiler will silently bail on components it can&rsquo;t safely transform. That&rsquo;s correct behavior, since an unmemoized component beats a wrong one, but it means you can ship something you think is compiled and have a quarter of your tree opt out without knowing.<\/p>\n<p>The fix is <code>eslint-plugin-react-compiler<\/code>. Turn it on with <code>error<\/code>, not <code>warn<\/code>. It flags every place the compiler had to bail, which is mostly violations of the <a href=\"https:\/\/react.dev\/reference\/rules\" rel=\"nofollow noopener\" target=\"_blank\">Rules of React<\/a>: mutating a value during render, calling hooks conditionally, that kind of thing.<\/p>\n<pre><code class=\"language-jsonc\">\/\/ .eslintrc.json\n{\n  &quot;plugins&quot;: [&quot;react-compiler&quot;],\n  &quot;rules&quot;: {\n    &quot;react-compiler\/react-compiler&quot;: &quot;error&quot;\n  }\n}\n<\/code><\/pre>\n<p>In my codebase the lint pass surfaced about thirty bail-outs. Most were genuine bugs in waiting: a util that mutated its argument, a couple of components reading from a singleton mid-render. Two were false positives where the compiler is conservative about destructuring patterns. I rewrote those rather than disable the rule per file, because once you start sprinkling <code>\/\/ eslint-disable<\/code> you stop trusting the signal.<\/p>\n<h2 id=\"turning-it-on-in-nextjs-and-vite\">Turning it on in Next.js and Vite<\/h2>\n<p>For Next.js (App Router, version 15.1+), it&rsquo;s a config flag:<\/p>\n<pre><code class=\"language-js\">\/\/ next.config.js\nmodule.exports = {\n  experimental: {\n    reactCompiler: true,\n  },\n};\n<\/code><\/pre>\n<p>For Vite, install the babel plugin and add it to the React plugin&rsquo;s babel options:<\/p>\n<pre><code class=\"language-js\">\/\/ vite.config.js\nimport { defineConfig } from 'vite';\nimport react from '@vitejs\/plugin-react';\n\nexport default defineConfig({\n  plugins: [\n    react({\n      babel: {\n        plugins: [['babel-plugin-react-compiler', {}]],\n      },\n    }),\n  ],\n});\n<\/code><\/pre>\n<p>A few things I learned the hard way. Don&rsquo;t bother with the &ldquo;compile only annotated components&rdquo; mode in a real codebase, because you&rsquo;ll forget. Compile everything and let the bail-outs surface in lint. Build times went up about 12% on a 400-component app, which is not nothing but not enough to argue about. The bundle size went up by maybe 4 KB after gzip in my case, which is the inserted memo cache. That&rsquo;s a small price to pay for not chasing stale closures.<\/p>\n<h2 id=\"should-you-turn-it-on\">Should you turn it on<\/h2>\n<p>If you&rsquo;re on React 19 and you&rsquo;ve been writing <code>useMemo<\/code> and <code>useCallback<\/code> defensively, yes. The compiler does that work better than you do, and you&rsquo;ll find a few real bugs along the way.<\/p>\n<p>If you&rsquo;re still on React 18, the compiler does work there with a runtime polyfill, but the gains are smaller and the upgrade path to 19 is the bigger fish. Spend the time on that first.<\/p>\n<p>If you have a custom renderer, a heavy reliance on third-party hooks, or a codebase that&rsquo;s been violating the Rules of React for a long time, expect a week of cleanup before things settle. That&rsquo;s not a reason to skip it. That&rsquo;s a reason to do it now, while the tooling is fresh and the bail-out reports are short.<\/p>\n<h2 id=\"what-to-do-this-week\">What to do this week<\/h2>\n<p>One concrete thing you can run this week: install <code>eslint-plugin-react-compiler<\/code>, set it to <code>warn<\/code>, and look at the report. You don&rsquo;t have to flip the compiler on yet. The lint output alone is worth the half-hour, because every bail-out is either a real bug or a place the compiler is being conservative about something you can simplify.<\/p>\n<p>I write a lot about this kind of incremental adoption in <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">the rest of my work<\/a>. The pattern is always the same: ship the diagnostic before you ship the fix. The compiler is a fix you can ship two weeks after you&rsquo;ve seen what your codebase actually looks like through it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>I spent a Sunday turning on the React Compiler in a real Next.js app. Here&#8217;s what I deleted, what bit me, and the eslint rule that actually matters.<\/p>\n","protected":false},"author":2,"featured_media":206,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"I spent a Sunday turning on the React Compiler in a real Next.js app. Here's what I deleted, what bit me, and the eslint rule that actually matters.","rank_math_focus_keyword":"react compiler","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[138,35],"tags":[38,44,243,19,41,43,42],"class_list":["post-207","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-frontend","category-web-development","tag-frontend","tag-javascript","tag-memoization","tag-performance","tag-react","tag-react-19","tag-react-compiler"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/207","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=207"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/207\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/206"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=207"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=207"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=207"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}