Why I Stopped Using Frameworks for Everything

TL;DR

Frameworks solve real problems but add complexity. Use them when they solve problems you actually have, not because they're trendy. Sometimes vanilla code is faster to write and maintain.

I used to be a framework addict. React for everything, even static pages. Express for APIs that returned three JSON endpoints. Bootstrap for a simple contact form. If there was a framework that could do something, I'd use it.

Then I spent a weekend debugging why my "simple" blog was taking 4 seconds to load, only to discover I was shipping 200KB of JavaScript to render what was essentially static HTML. That's when I started questioning everything.

Three years later, I'm way more selective about frameworks. I still use them, but only when they actually solve problems I have. Here's what changed my mind and when I reach for vanilla code instead.

The Framework Trap I Fell Into

My turning point was a client project - a basic landing page with a contact form. My first instinct was to reach for React because, well, that's what I knew.

// My original approach - 47KB gzipped for THIS
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'react-toastify';

function ContactForm() {
  const { register, handleSubmit, errors } = useForm();
  const [loading, setLoading] = useState(false);
  
  const onSubmit = async (data) => {
    setLoading(true);
    // Submit form...
    setLoading(false);
    toast.success('Message sent!');
  };
  
  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('email', { required: true })} />
      {errors.email && <span>Email required</span>}
      <button disabled={loading}>
        {loading ? 'Sending...' : 'Send'}
      </button>
    </form>
  );
}

Bundle size: 47KB gzipped. Dependencies: React, React-DOM, React-Hook-Form, React-Toastify.

Then I rewrote it in vanilla JavaScript:

<!-- 2KB total -->
<form id="contact-form">
  <input type="email" name="email" required>
  <button type="submit">Send</button>
  <div id="status"></div>
</form>

<script>
document.getElementById('contact-form').addEventListener('submit', async (e) => {
  e.preventDefault();
  const button = e.target.querySelector('button');
  const status = document.getElementById('status');
  
  button.disabled = true;
  button.textContent = 'Sending...';
  
  try {
    const response = await fetch('/contact', {
      method: 'POST',
      body: new FormData(e.target)
    });
    
    if (response.ok) {
      status.textContent = 'Message sent!';
      e.target.reset();
    } else {
      status.textContent = 'Error sending message';
    }
  } catch (error) {
    status.textContent = 'Error sending message';
  } finally {
    button.disabled = false;
    button.textContent = 'Send';
  }
});
</script>

Same functionality. 2KB instead of 47KB. No build step. No dependencies. Loads instantly.

That was my "oh shit" moment.

When I Actually Need Frameworks

Don't get me wrong - frameworks solve real problems. I still use them, but I'm way more intentional about it.

Complex State Management

When I'm building something with genuinely complex state interactions, React's mental model is invaluable:

// This complexity justifies React
function ShoppingCart() {
  const [items, setItems] = useState([]);
  const [discount, setDiscount] = useState(null);
  const [shipping, setShipping] = useState(null);
  
  // Complex derived state
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  const discountAmount = discount ? subtotal * discount.rate : 0;
  const tax = (subtotal - discountAmount) * 0.08;
  const total = subtotal - discountAmount + tax + (shipping?.cost || 0);
  
  // Multiple components need to trigger state changes
  return (
    <div>
      <ItemList items={items} onUpdateQuantity={updateQuantity} />
      <DiscountCode onApply={setDiscount} />
      <ShippingOptions onSelect={setShipping} />
      <OrderSummary {...{subtotal, discountAmount, tax, total}} />
    </div>
  );
}

Managing this state and keeping the UI in sync manually would be a nightmare. React earns its keep here.

Large Teams and Codebases

When I'm working with a team of 8+ developers on a codebase that'll live for years, frameworks provide structure and conventions that prevent chaos:

// Framework provides structure for large teams
interface UserProfileProps {
  user: User;
  onUpdate: (user: User) => void;
}

const UserProfile: React.FC<UserProfileProps> = ({ user, onUpdate }) => {
  // Clear contracts, TypeScript integration, testability
};

Everyone knows where things go, how to structure components, and what patterns to follow.

When Vanilla Code Is Better

Static or Mostly-Static Sites

For blogs, marketing sites, documentation - anything that's primarily static content with light interactivity:

<!-- Hugo/Jekyll template is often better than Gatsby -->
{{ range .Pages }}
  <article>
    <h2>{{ .Title }}</h2>
    <p>{{ .Summary }}</p>
  </article>
{{ end }}

<!-- Add interactivity where needed -->
<script>
// Just the JavaScript you actually need
document.querySelectorAll('.toggle').forEach(toggle => {
  toggle.addEventListener('click', () => {
    toggle.nextElementSibling.classList.toggle('hidden');
  });
});
</script>

Faster builds, faster loading, easier deployment, better SEO out of the box.

Simple APIs

When I need a basic REST API, Express often feels like overkill:

// Express version
const express = require('express');
const app = express();

app.use(express.json());
app.get('/users/:id', (req, res) => {
  res.json({ id: req.params.id });
});

app.listen(3000);
// Node.js built-in version
const http = require('http');
const url = require('url');

http.createServer((req, res) => {
  const { pathname } = url.parse(req.url);
  const match = pathname.match(/^\/users\/(\d+)$/);
  
  if (match) {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ id: match[1] }));
  } else {
    res.writeHead(404);
    res.end();
  }
}).listen(3000);

For simple APIs, the built-in approach is often more explicit and has fewer dependencies.

Performance-Critical Code

When every millisecond matters, vanilla often wins:

// Lodash version
const result = _.chain(data)
  .filter(item => item.active)
  .map(item => item.value)
  .sum()
  .value();

// Vanilla version - 3x faster
let result = 0;
for (let i = 0; i < data.length; i++) {
  if (data[i].active) {
    result += data[i].value;
  }
}

The vanilla version is more verbose but significantly faster for large datasets.

The Mental Shift That Changed Everything

The key insight was asking "What problem am I solving?" before reaching for any tool.

Bad reasons to use a framework:

  • "It's what I know"
  • "It's trendy"
  • "We might need these features later"
  • "Everyone else uses it"

Good reasons to use a framework:

  • "This solves a specific problem I have right now"
  • "The complexity it adds is less than the complexity it removes"
  • "My team needs the structure and conventions"
  • "The performance trade-off is worth the developer experience"

My Current Decision Framework

For Frontend Projects:

Static site with minimal interactivity: Hugo/Jekyll + vanilla JS Marketing site with some dynamic features: Vanilla JS or Alpine.js Dashboard or admin panel: React/Vue (state management justifies the complexity) Complex user-facing app: React/Vue with full toolchain

For Backend Projects:

Simple API (< 10 endpoints): Node.js built-ins or Go standard library Standard REST API: Express/Fastify or Gin/Echo Complex business logic with auth/validation/etc: Full framework like NestJS or Django High-performance requirements: Go standard library or Rust

The CSS Decision Tree:

Simple styling: Vanilla CSS Component-based styling with reusable patterns: CSS custom properties + utility classes Large team or design system: Tailwind CSS Complex state-dependent styling: CSS-in-JS (but only then)

What I've Gained from This Approach

Faster initial development: No setup time, no configuration, no build step for simple projects.

Better performance: Smaller bundles, faster load times, less JavaScript to parse.

Easier maintenance: Fewer dependencies to update, less complexity to debug.

Deeper understanding: I actually know how things work instead of just knowing framework APIs.

Better framework usage: When I do use frameworks, I use them more intentionally and effectively.

The Surprising Benefits

I became a better React developer by understanding what React is actually doing under the hood.

My vanilla JavaScript skills improved dramatically because I was writing it regularly instead of always reaching for abstractions.

I ship features faster because I'm not fighting with build tools and dependency conflicts for simple tasks.

My apps are more reliable because there are fewer moving parts that can break.

When This Approach Doesn't Work

Large teams without senior guidance: Frameworks provide guardrails that prevent junior developers from creating unmaintainable code.

Rapidly changing requirements: Sometimes the framework's abstraction layer makes pivots easier.

Complex domains: Some problems are inherently complex and need sophisticated tooling.

Time pressure with familiar tools: If you know React inside and out, it might be faster than learning vanilla approaches.

The Middle Ground: HTMX Changed My Mind About SPAs

I've found a sweet spot with minimal, focused libraries that solve specific problems:

// Instead of full frameworks, targeted solutions
import Alpine from 'alpinejs';  // For interactive components
import htmx from 'htmx.org';    // For server interactions
import { format } from 'date-fns/format';  // Just the functions I need

But HTMX deserves special mention because it completely changed how I think about web applications.

HTMX: The Framework That Isn't

HTMX lets you build dynamic web apps with mostly HTML and server-side code. Instead of shipping JavaScript frameworks to the browser, you enhance HTML with attributes:

<!-- This replaces an entire React component -->
<button hx-post="/toggle-like" 
        hx-target="#like-count"
        hx-swap="innerHTML">
  ❤️ Like
</button>

<span id="like-count">42 likes</span>

When clicked, it makes a POST request and updates the DOM with the server response. No JavaScript bundle, no state management, no complex build process.

Real Example: Search That Actually Works

I replaced a React search component with this:

<!-- The entire search interface -->
<input type="text" 
       placeholder="Search users..."
       hx-get="/search"
       hx-trigger="keyup changed delay:300ms"
       hx-target="#results"
       hx-indicator="#spinner">

<div id="spinner" class="htmx-indicator">Searching...</div>
<div id="results"></div>

Server returns HTML fragments:

<!-- /search endpoint returns this -->
<div class="user">John Doe - john@example.com</div>
<div class="user">Jane Smith - jane@example.com</div>

That's it. No useState, no useEffect, no debouncing logic, no JSON parsing. The server does the search and returns ready-to-display HTML.

Why This Approach Wins

Simpler mental model: Instead of managing client and server state separately, the server is the single source of truth.

Better performance: No JavaScript bundle to download, parse, and execute. The browser just renders HTML.

Easier debugging: View source shows you exactly what's happening. No client-side routing mysteries.

Progressive enhancement: Works without JavaScript, gets enhanced with HTMX.

Team productivity: Backend developers can build full features without learning React.

I've built entire admin dashboards with HTMX that feel as responsive as SPAs but with 90% less complexity.

My Advice

Start with the simplest solution that could work. You can always add complexity later, but removing it is much harder.

Learn vanilla first. Understanding the underlying platform makes you better at using abstractions.

Question your defaults. Just because you used React for the last project doesn't mean it's right for this one.

Measure what matters. If bundle size doesn't matter for your use case, don't optimize for it. If development speed is critical, maybe the framework overhead is worth it.

The goal isn't to avoid frameworks - it's to use them intentionally when they solve real problems you actually have.

I still love React. I still use it regularly. But I also love shipping a 2KB contact form that loads instantly instead of a 47KB React app that does the same thing.

The best tool is the simplest one that gets the job done well.