Why I Quit React
And What I Use Now
I used React professionally for years. I built production apps with it, shipped side projects with it, taught other people how to use it. And then, sometime in 2024, I just… stopped. Not in a dramatic way. I didn’t mass-delete my node_modules and post about it on Twitter. That would have been iconic, though. I just started a new project and didn’t reach for it. And then the next project. And the next one.
Now my personal site (the one you’re reading right now) has WebGL particle systems, SPA navigation with animated page transitions, a prefetch cache, a Web Worker pipeline, and not a single framework. Vanilla TypeScript compiled with Vite. Every byte is mine. 🎉
Here’s why.
What the Virtual DOM Actually Does
Let’s start with the thing React is famous for: the virtual DOM. The pitch is elegant. Instead of touching the real DOM directly (which is slow), you describe what you want the UI to look like, React diffs that description against the previous one, and it applies the minimum set of changes. Reconciliation.
The problem is that this pitch contains a buried assumption: that diffing a JavaScript object tree is cheaper than just updating the DOM. And for a long time, on the hardware and browsers of 2013, it was! The real DOM was genuinely terrible. innerHTML was a minefield. Event delegation was a nightmare. The virtual DOM was a brilliant workaround for a platform that wasn’t ready.
But the platform grew up. And React kind of… didn’t.
Modern browsers batch DOM updates within a single frame. requestAnimationFrame gives you a reliable paint cycle. The MutationObserver API lets you watch changes efficiently. CSS containment tells the browser which subtrees can be skipped during layout. The DOM isn’t the bottleneck it was a decade ago, and yet we’re still paying the cost of diffing a shadow tree on every state change. Cool. Love that for us.
That cost isn’t zero. React’s reconciler walks the entire component subtree on every render. It allocates fiber nodes. It schedules work across priority lanes. It runs effects. For a complex interactive page, you can easily spend more time in the reconciler than you would have spent just setting .textContent on six elements. The framework is protecting you from a problem the browser already solved, and charging you for the privilege. Thanks, I hate it!
What Vite Gives You Without a Framework
Here’s the thing people don’t realize: most of what made React feel productive had nothing to do with React. It was the tooling. Hot Module Replacement. ES module imports. Tree-shaking dead code. TypeScript compilation. JSX was just syntactic sugar over createElement calls, and you don’t need JSX to write clean, composable UI code.
Vite gives you all of that without the framework. I write .ts files, import them with standard ES module syntax, and Vite handles the rest. HMR is instant. Tree-shaking is automatic. TypeScript is first-class. My dev server starts in under 200ms because there’s no framework bootstrap, no provider trees, no compiler plugins. It’s beautiful!
My production builds are tiny. Not “small for a React app” tiny. Actually tiny. The entire JavaScript bundle for this site (including WebGL shaders, a full SPA router, an AI chat widget, and particle systems) ships less JS than a bare create-react-app scaffold with zero components. Let that sink in for a second.
A Real Example: SPA Navigation
Let me show you something concrete. This site is a full single-page application. When you click a link, the page doesn’t reload. The content transitions in with animations. Previous pages are cached. Links are prefetched on hover. Back/forward navigation works. It’s everything React Router gives you.
Here’s the core of it:
document.addEventListener('click', (e) => {
const link = (e.target as Element).closest('a');
if (!link) return;
const href = link.getAttribute('href');
if (!href || href.startsWith('http') || href.startsWith('#')) return;
e.preventDefault();
this.navigate(href);
});
window.addEventListener('popstate', () => {
this.navigate(location.pathname, false);
});
That’s the router. The whole thing! Intercept clicks on internal links, call navigate(), handle popstate for browser back/forward. No <BrowserRouter>. No <Route path="/">. No useNavigate hook. Just the History API, which has been stable in every browser since 2014. It’s been sitting right there this whole time.
The navigate method fetches the target page, parses it with DOMParser, swaps the content into the existing DOM, and updates document.title. Prefetching happens on hover via a mouseenter listener that fires off a low-priority fetch and stashes the parsed result in a Map. A Web Worker handles the HTML parsing off the main thread so it never blocks interaction.
private prefetch(href: string): void {
if (this.prefetchCache.has(href) || this.prefetchingUrls.has(href)) return;
this.prefetchingUrls.add(href);
this.worker.postMessage({ type: 'parse', url: href });
}
When the worker finishes parsing, the result goes into a hidden div in the DOM: a prerender cache. By the time you click, the content is already parsed, laid out, and waiting. The transition is instant because there’s nothing left to do except swap it in and animate. Chef’s kiss.
Could I have built this in React? Sure. It would’ve taken longer, shipped more bytes, and I would have spent half the time fighting the framework’s opinion about how navigation should work. React Router has its own ideas about data loading, route matching, and transition timing. I didn’t want its ideas. I wanted mine.
Web Components: The Platform’s Answer
I’m not anti-component. I’m anti-framework-component. The browser has its own component model now, and it’s actually good!
Custom Elements give you encapsulated, reusable UI with lifecycle hooks ( connectedCallback, disconnectedCallback, attributeChangedCallback ). Shadow DOM gives you style isolation. HTML templates give you declarative structure. These are standards. They work in every browser. They don’t need a build step. They’ll still be here when React 47 drops and everyone has to rewrite their apps again.
I don’t use Web Components for everything; for simple UI, a function that returns a DocumentFragment is fine. But when I need a self-contained widget with its own lifecycle, Custom Elements are right there, and they’ll still work in ten years. No migration guide required.
The Honest Tradeoff
I’m not going to pretend this approach is universally better. It isn’t. I’m opinionated, not delusional.
If you’re on a team of fifteen engineers with varying experience levels, a framework gives you shared conventions, a common mental model, and guard rails that prevent people from making expensive mistakes. React’s one-way data flow and component model aren’t just technical choices; they’re communication tools. They make code reviews faster because everyone agrees on the shape of things.
If you’re building a complex application with deeply nested state (think Figma, or a spreadsheet, or a real-time collaborative editor), React’s reconciliation model and the ecosystem around it (state machines, data fetching libraries, dev tools) solve real problems that you’d otherwise have to solve yourself. And solving them yourself is a multi-month detour that most teams can’t afford. Ask me how I know. 😅
If you need server-side rendering, streaming, or a meta-framework’s worth of routing and data loading conventions, Next.js and Remix exist for good reasons. I’m not building e-commerce sites with vanilla JS. That would be masochism.
But here’s the thing: most of us aren’t building Figma. Most side projects, portfolios, creative experiments, blogs, and small tools don’t need a virtual DOM. They don’t need a reconciler. They don’t need a 200KB runtime to toggle a class name. Come on!!
The Browser Is the Framework
The web platform in 2025 is unrecognizable from the platform that made React necessary. We have:
- ES Modules natively in the browser (and Vite for the dev experience)
- View Transitions API for animated page transitions
- Custom Elements for reusable components
- CSS Container Queries for responsive design without JS
requestAnimationFrameand the Web Animations API for smooth motion- Web Workers for off-main-thread computation
IntersectionObserver,ResizeObserver,MutationObserverfor reactive behavior
These aren’t experimental. They’re shipped, stable, and fast. When I need reactivity, I use an EventTarget. When I need templating, I use template literals. When I need routing, I use the History API. When I need animation, I use the Web Animations API. The browser isn’t a hostile environment anymore. It’s a runtime. 🚀
The best code is code you understand completely, that does exactly what you need, and that you can change without asking permission from an abstraction layer. For my projects (creative, visual, personal), going frameworkless didn’t just save bytes. It made programming fun again.
Your mileage will vary. But if you’ve never tried building something real without a framework, with modern tooling and modern browser APIs, you might be surprised how little you miss. I was!