Why We Migrated from React to Vanilla JS (And Why We Don't Regret It)

It started with a simple bug. A state mismatch in our dashboard that took three senior engineers four hours to trace through a maze of context providers, custom hooks, and reconciliation cycles.

We stared at the screen, exhausted. The app was sluggish. The bundle size was creeping toward the megabyte mark. And for what? A set of interactive forms, a handful of dynamic charts, and some real-time updates.

That night, we asked ourselves a radical question: Do we actually need React for this?

The answer was a resounding "no." So we decided to rewrite. We ripped React out, replaced it with pure, unadulterated vanilla JavaScript, and we haven't looked back since. Here is why we did it, how it went, and why we don't regret it for a second.

Relieved developer celebrating clean Vanilla JS code


The Framework Trap

Don't get me wrong. React is a marvel of engineering. It changed how we think about UI. But somewhere along the line, the industry decided that you cannot build a modern web application without a heavy client-side framework.

We fall into the "Framework Trap":

  • We add React to handle UI rendering.
  • We add a state management library (Redux, Zustand, Recoil) because context propagates too many re-renders.
  • We add compiler configurations, bundle analyzers, and optimization plugins to fix the performance issues introduced by the framework itself.
  • We download thousands of npm packages to do things the browser can already do natively.

We were building a temple of abstraction on top of a foundation of quicksand.


The Migration: React vs. Vanilla JS

Our application, totop.app, runs complex coding simulators and interactive interview sessions. Performance isn't just a vanity metric for us—it directly impacts candidate scores and system responsiveness.

When we compared our old React implementation against our new Vanilla JS rewrite, the numbers were shocking.

Comparison between tangled React bundle and direct Vanilla JS flow

1. Bundle Size: 1.8MB down to 12KB

Our React build had ballooned. Between the React runtime, component libraries, and custom hooks, our bundle size was 1.8MB.

Our Vanilla JS replacement? A single, highly optimized script file that weighs exactly 12KB (gzipped). It loads instantly, even on the spotty 3G connections our candidates sometimes use during remote interviews.

2. Interaction to Next Paint (INP): From 180ms to 4ms

With React, every button click triggered a reconciliation cycle. The virtual DOM had to diff the entire page tree to figure out what changed. On low-end mobile devices, this created noticeable input lag.

By directly manipulating the DOM using vanilla APIs, our response times dropped to 4ms. The UI feels incredibly snappy, responsive, and alive.

3. Debugging Complexity: No Magic, Just Code

In React, when a state update doesn't trigger a re-render, you have to debug the reconciliation engine. You look at dependency arrays, check object references, and pray you didn't mutate state directly.

In Vanilla JS, if a DOM node doesn't update, you find the line of code that sets .textContent or .innerHTML and fix it. There is no magic. It is just JavaScript.


How We Rebuilt Without React

People often ask: How do you manage complex UI state without React's component architecture?

It turns out the browser has gotten incredibly good at this while we were all looking at frameworks. Here are the core browser APIs we leveraged to build a robust, maintainable application without React:

1. Native Web Components

You don't need React to create reusable components. The HTML5 Web Components standard is fully supported in all major browsers:

class CodeViewer extends HTMLElement {
  connectedCallback() {
    const code = this.getAttribute('code');
    this.innerHTML = `
      <div class="code-container">
        <pre><code>${this.escapeHTML(code)}</code></pre>
      </div>
    `;
  }
  
  escapeHTML(str) {
    return str.replace(/[&<>'"]/g, 
      tag => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' }[tag] || tag)
    );
  }
}

customElements.define('code-viewer', CodeViewer);

2. Simple Pub/Sub for State Management

Instead of heavy state libraries, we wrote a lightweight Pub/Sub store in 15 lines of code:

class Store {
  constructor(initialState) {
    this.state = initialState;
    this.listeners = [];
  }

  subscribe(listener) {
    this.listeners.push(listener);
    return () => { this.listeners = this.listeners.filter(l => l !== listener); };
  }

  setState(newState) {
    this.state = { ...this.state, ...newState };
    this.listeners.forEach(listener => listener(this.state));
  }
}

This simple store manages the active state across our coding dashboard without a single dependency.


The Trade-offs: Why This Isn't for Everyone

We don't regret our migration, but we also won't pretend it was easy or that it's the right choice for every team.

  • You write more boilerplate: You have to manually handle DOM creation, event listeners, and cleanup.
  • You need high discipline: Without a framework enforcing a structure, a junior-heavy team can easily turn a Vanilla JS codebase into spaghetti code.
  • Ecosystem loss: You lose access to the massive library of pre-built React components. If you need a complex modal or date picker, you often have to build it from scratch.

At totop.app, we believe that solid developer fundamentals beat framework knowledge every single time. That's why we focus our technical interview prep on raw coding, algorithms, and system design, rather than framework-specific syntax.


Final Thoughts: The Web is Fast, Frameworks are Slow

Migrating to Vanilla JS taught us that the modern web platform is incredibly capable. We don't hate React, but we realized we were using it as a default safety blanket rather than a calculated tool.

If your application is massive, has hundreds of developers working on it, or relies heavily on dynamic third-party widgets, React makes total sense.

But if you are building a product where speed, load times, and low latency are critical to user success, take a second look at vanilla JavaScript. You might be surprised at how fast your app—and your developers—can run without a framework dragging them down.