Before diving into this article, I have to say that I haven't been a front-end developer for a very, very long time 😁 I just analyze architectures and enjoy listening to people who argue about what's best to use on the front end. So I might be wrong about some things. 🤷♂️🤷♂️🤷♂️ Please correct me if I'm wrong!
Recently, I came across a team that was rewriting a project from Vue to Ract, and their argument was that Vue runs slower. 😬 This made me wonder, why does Vue run slower? And it turned out that the Vue code had been generated but not optimized.
And while there were a lot of questions about the code generation itself in the Vue project, the team didn’t have any questions at all about the generated code in the React project because everything works well there. 🤔🤨 But I won’t go into that right now, I’d rather analyze everything in detail and describe it in another article.
I won't go into a detailed comparison either, since there's plenty of that online. I'll just list the three main popular aspects below. Some add a fourth about templates versus JSX. That one's taste, so I'll leave it alone.
| | React | Vue | |---|---|---| | Virtual DOM | yes | yes | | Memoization | by hand, with memo, useMemo and useCallback | automatic, through reactivity | | What re-renders | the component and its children, unless you stop it | only what read the state that changed |
Every row in that table is out of date or about to be. React now ships a compiler that writes the memoization for you, and Vue is finishing a compile mode that drops the virtual DOM for any component that opts in. The two of them started from opposite ends of the same old argument and ended up in roughly the same spot: a build step that works out what doesn't need to run.
That's why I think this is the first year the interesting React vs Vue question isn't a technical one. They aren't the same framework. There's a whole section below on where they still split, but the three talking points everyone reaches for are exactly the three that are dissolving.
React's default is simple and a bit blunt. The memo docs say it in one line: "React normally re-renders a component whenever its parent re-renders." For years the fix was to tell React by hand what was safe to skip. Here's what that looks like in a small product list with a search box and rows you can pick:
There are three tools here and two of them only work as a pair. memo lets a row skip rendering when its props look the same as last time. useCallback keeps onPick the same function between renders. That part matters. A fresh function counts as a changed prop and quietly defeats the memo. Forget the useCallback and the memo still compiles, still passes review, still looks perfectly responsible in the diff. It just skips nothing. useMemo is the odd one out. It keeps the filter from running again when the only thing that changed is picked.
The React team was blunter about all this than I'd be, and in the React Labs post from February 2024 they wrote that "manual memoization is a compromise. It clutters up our code, is easy to get wrong, and requires extra work to keep up to date."
React Compiler 1.0 shipped in October 2025 and works with React 17 and up. For new code the release post recommends "relying on the compiler for memoization and using useMemo/useCallback where needed to achieve precise control." Expo has had it on by default since SDK 54. Next.js 16 made the reactCompiler option stable but left it off by default while the team gathers build performance data (they also warn that builds get slower with it on, since the compiler runs through Babel, and the Rust port that runs inside Turbopack only showed up in Next.js 16.3 in August 2026 as an experimental flag). So "React memoizes for you now" is true, with a footnote: on the most popular React framework, somebody still has to flip a switch first.
What matters for this comparison is the component with the compiler on, next to the Vue version of the same thing:
The React version's just the first one with every wrapper deleted. The Vue version never had wrappers to delete.
I ran that plain React file through babel-plugin-react-compiler 1.0.0, because I wanted to see what it actually produces rather than trust the diagrams, and the answer is a lot of bookkeeping: ProductList gets 21 cache slots, ProductRow gets 6 of its own, and every one of them is a plain array entry checked by an if. Here's the part of ProductList that matters, trimmed down:
That Symbol.for("react.memocachesentinel") check is the compiler's version of useCallback(fn, []). The first render stores the function in slot 8 and every render after that reads it back. Nobody wrote a dependency array.
The filter's where it gets interesting. My hand-written useMemo was keyed on products and query. The compiler put products.filter(...) inside a block that's also keyed on picked. So picking a row runs the filter again. With fifty products nobody will ever notice. If the filter were genuinely slow, this is the case the release post means when it says to keep useMemo for "precise control", because the compiler caches what flows into the output and its idea of a sensible cache boundary (one big block keyed on everything the JSX reads, as far as I can tell from this output) isn't always the one I'd have drawn by hand.
None of this was ever a question in the Vue version. The reason is how Vue decides what to run. Its docs describe it plainly: "each component instance creates a reactive effect to render and update the DOM." The render reads query, picked and visible, Vue records those reads, and only a change to one of them re-runs this render. computed caches its value and only re-runs the filter when products or query changes, so picking a row doesn't re-filter and I never had to say so.
Children follow a matching rule. Vue's performance guide says "a child component only updates when at least one of its received props has changed."
