Back to blog

Vanilla JavaScript in 2026: Why Developers Are Abandoning Frameworks

Hello HaWkers, something interesting is happening in the JavaScript ecosystem: experienced developers are increasingly opting for pure JavaScript instead of complex frameworks. What was once seen as "going back to the past" is now considered a strategic and sophisticated choice.

Let's understand this movement, when it makes sense to use vanilla JavaScript, and how modern browser APIs have made this viable in 2026.

Framework Fatigue

Framework Fatigue Is Real

The JavaScript community is exhausted from constant changes.

Symptoms of framework fatigue:

  • New promising framework every 6 months
  • Major updates with frequent breaking changes
  • Time spent learning instead of building
  • Dependencies that become vulnerabilities
  • Build tools that constantly change

Timeline of complexity:

Year What Was Needed
2010 jQuery
2014 Gulp, Bower, Angular 1.x
2016 Webpack, React, Redux, Babel
2018 Create React App, TypeScript, CSS-in-JS
2020 Next.js, Vite, Tailwind, State machines
2022 Server Components, Edge Functions, Hydration
2024 AI coding assistants, Meta-frameworks

The problem:

"I spent more time configuring build tools than writing product code." - Anonymous developer in 2025 survey

Modern JavaScript Is Powerful

The APIs That Changed Everything

The browser has evolved dramatically. Many reasons to use frameworks simply no longer exist.

Element selection (old problem jQuery solved):

// Before: we needed jQuery
$('.cards .item');

// Today: native and equally simple
document.querySelectorAll('.cards .item');

// With optional helper
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => [...document.querySelectorAll(sel)];

// Usage
const items = $$('.cards .item');
items.forEach(item => item.classList.add('active'));

Class manipulation:

// Old way: jQuery or complex code
element.className = element.className.replace('old', 'new');

// Today: clean and powerful classList API
element.classList.add('new');
element.classList.remove('old');
element.classList.toggle('active');
element.classList.replace('old', 'new');
element.classList.contains('active'); // boolean

Fetch API vs HTTP libraries:

// Modern fetch with async/await
async function fetchUsers() {
  try {
    const response = await fetch('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ page: 1 }),
    });

    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Fetch failed:', error);
    throw error;
  }
}

// With AbortController for cancellation
const controller = new AbortController();

fetch('/api/data', { signal: controller.signal })
  .then(res => res.json())
  .then(data => console.log(data));

// Cancel the request
controller.abort();

Web Components: Native Components

Creating Components Without a Framework

Web Components allow real component encapsulation.

// Defining a custom component
class UserCard extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  // Observed attributes for reactivity
  static get observedAttributes() {
    return ['name', 'email', 'avatar'];
  }

  // Lifecycle: when connected to DOM
  connectedCallback() {
    this.render();
  }

  // Lifecycle: when attribute changes
  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue !== newValue) {
      this.render();
    }
  }

  render() {
    const name = this.getAttribute('name') || 'Anonymous';
    const email = this.getAttribute('email') || '';
    const avatar = this.getAttribute('avatar') || '/default-avatar.png';

    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          padding: 1rem;
          border-radius: 8px;
          background: var(--card-bg, #fff);
          box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        .card {
          display: flex;
          align-items: center;
          gap: 1rem;
        }
        img {
          width: 48px;
          height: 48px;
          border-radius: 50%;
        }
        h3 {
          margin: 0 0 0.25rem;
          font-size: 1.1rem;
        }
        p {
          margin: 0;
          color: #666;
          font-size: 0.9rem;
        }
      </style>
      <div class="card">
        <img src="${avatar}" alt="${name}">
        <div>
          <h3>${name}</h3>
          <p>${email}</p>
        </div>
      </div>
    `;
  }
}

// Register the component
customElements.define('user-card', UserCard);

Usage in HTML:

<user-card
  name="Jeff Bruchado"
  email="jeff@example.com"
  avatar="/jeff.jpg">
</user-card>

<!-- Interacting via JavaScript -->
<script>
  const card = document.querySelector('user-card');
  card.setAttribute('name', 'New Name');
</script>

State Without Redux

Native State Management

You don't need Redux or Context API to manage state.

// Simple store with Proxy for reactivity
function createStore(initialState) {
  const listeners = new Set();

  const state = new Proxy(initialState, {
    set(target, property, value) {
      target[property] = value;
      listeners.forEach(listener => listener(state));
      return true;
    }
  });

  return {
    getState: () => state,
    subscribe: (listener) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
    setState: (updates) => {
      Object.assign(state, updates);
    }
  };
}

// Usage
const store = createStore({
  user: null,
  cart: [],
  loading: false
});

// Subscribing to changes
store.subscribe((state) => {
  console.log('State changed:', state);
  updateUI(state);
});

// Updating state
store.setState({ loading: true });
store.setState({ user: { name: 'Jeff' }, loading: false });

State with Custom Events:

// Event-driven state management
class StateManager extends EventTarget {
  #state = {};

  constructor(initialState = {}) {
    super();
    this.#state = { ...initialState };
  }

  get state() {
    return { ...this.#state };
  }

  setState(updates) {
    const prevState = { ...this.#state };
    this.#state = { ...this.#state, ...updates };

    this.dispatchEvent(new CustomEvent('statechange', {
      detail: { prevState, state: this.state }
    }));
  }
}

// Usage
const appState = new StateManager({ count: 0 });

appState.addEventListener('statechange', (e) => {
  const { state } = e.detail;
  document.querySelector('#count').textContent = state.count;
});

document.querySelector('#increment').onclick = () => {
  appState.setState({ count: appState.state.count + 1 });
};

Client-Side Routing

SPA Routing Without a Framework

The History API allows creating SPAs without React Router or Vue Router.

// Minimalist router
class Router {
  constructor() {
    this.routes = new Map();
    this.currentRoute = null;

    // Intercept link clicks
    document.addEventListener('click', (e) => {
      if (e.target.matches('a[data-link]')) {
        e.preventDefault();
        this.navigate(e.target.href);
      }
    });

    // Handle back/forward button
    window.addEventListener('popstate', () => {
      this.handleRoute(window.location.pathname);
    });
  }

  addRoute(path, handler) {
    this.routes.set(path, handler);
    return this;
  }

  navigate(url) {
    const path = new URL(url, window.location.origin).pathname;
    window.history.pushState(null, '', path);
    this.handleRoute(path);
  }

  handleRoute(path) {
    // Matching with parameters
    for (const [routePath, handler] of this.routes) {
      const params = this.matchRoute(routePath, path);
      if (params !== null) {
        this.currentRoute = { path, params, handler };
        handler(params);
        return;
      }
    }

    // 404
    const notFound = this.routes.get('*');
    if (notFound) notFound({});
  }

  matchRoute(routePath, path) {
    const routeParts = routePath.split('/');
    const pathParts = path.split('/');

    if (routeParts.length !== pathParts.length) return null;

    const params = {};

    for (let i = 0; i < routeParts.length; i++) {
      if (routeParts[i].startsWith(':')) {
        params[routeParts[i].slice(1)] = pathParts[i];
      } else if (routeParts[i] !== pathParts[i]) {
        return null;
      }
    }

    return params;
  }

  start() {
    this.handleRoute(window.location.pathname);
  }
}

// Configuration
const router = new Router()
  .addRoute('/', () => renderHome())
  .addRoute('/users', () => renderUsers())
  .addRoute('/users/:id', ({ id }) => renderUser(id))
  .addRoute('*', () => render404());

router.start();

Performance: The Big Advantage

Impressive Numbers

Vanilla JavaScript sites tend to be significantly faster.

Bundle size comparison:

Approach Bundle Size TTI
React + Redux + Router ~150 KB ~2.5s
Vue + Vuex + Router ~100 KB ~2.0s
Svelte + Routing ~30 KB ~1.2s
Optimized Vanilla JS ~5-15 KB ~0.5s

Typical Core Web Vitals:

// Well-written vanilla JS
{
  LCP: '0.8s',  // Largest Contentful Paint
  FID: '10ms',  // First Input Delay
  CLS: '0.01',  // Cumulative Layout Shift
  TTI: '0.5s'   // Time to Interactive
}

// Typical framework
{
  LCP: '2.5s',
  FID: '100ms',
  CLS: '0.1',
  TTI: '3.0s'
}

Native lazy loading:

// Load modules on demand
async function loadModule(moduleName) {
  const modules = {
    charts: () => import('./modules/charts.js'),
    editor: () => import('./modules/editor.js'),
    admin: () => import('./modules/admin.js'),
  };

  const loader = modules[moduleName];
  if (!loader) throw new Error(`Module ${moduleName} not found`);

  return await loader();
}

// Usage
document.querySelector('#showChart').onclick = async () => {
  const { renderChart } = await loadModule('charts');
  renderChart('#chart-container', data);
};

When to Use Each Approach

The Right Framework for the Right Problem

Vanilla JavaScript isn't always the right answer.

Use vanilla JavaScript when:

  • Landing pages and institutional sites
  • Blogs and content sites
  • Simple applications with little interactivity
  • Embeddable widgets
  • Performance is top priority
  • You want to avoid dependencies

Use frameworks when:

  • Complex applications with lots of state
  • Large teams that need patterns
  • SPAs with dozens of screens
  • Need for mature ecosystem (UI libs, etc)
  • Team already experienced with the framework
  • Rapid prototyping

Hybrid approach:

// Use vanilla JS for the base
// Add specific libraries as needed

// For charts
import Chart from 'chart.js/auto';

// For complex dates
import { format, parseISO } from 'date-fns';

// For complex forms
// ... keep in vanilla or use specific lib

// No need for entire framework to use a lib

Resources to Learn More

Lightweight Tools and Libraries

If you want simplicity but need occasional help.

Minimalist libraries:

Library Size Purpose
Alpine.js 15 KB Simple reactivity
htmx 14 KB Declarative AJAX
Lit 5 KB Web Components
Preact 3 KB Minimal React-like
Petite Vue 6 KB Minimalist Vue

Modern polyfills (increasingly unnecessary):

<!-- Only if you need to support old browsers -->
<script src="https://polyfill.io/v3/polyfill.min.js?features=fetch,Promise,Array.from"></script>

Simple build tools:

// esbuild - extremely fast bundler
// package.json
{
  "scripts": {
    "build": "esbuild src/main.js --bundle --minify --outfile=dist/app.js"
  }
}

// No complex configuration, no endless plugins

Conclusion

The movement back to vanilla JavaScript isn't about rejecting progress, but about choosing the right tool for each job. In 2026, native JavaScript is powerful enough for most web applications, and many developers are discovering that fewer dependencies means fewer problems.

Key points:

  1. The modern browser is incredibly capable
  2. Web Components allow native componentization
  3. State management doesn't need a library
  4. Performance improves dramatically without frameworks
  5. The choice depends on the project context

Recommendations:

  • Evaluate if you really need a framework
  • Learn native APIs before frameworks
  • Consider a hybrid approach when it makes sense
  • Prioritize simplicity and long-term maintenance
  • Frameworks are still useful for complex cases

If you want to deepen your knowledge in modern JavaScript, I recommend reading: ES2026 New Features: What Changes in JavaScript.

Let's go! 🦅

Comments (0)

This article has no comments yet 😢. Be the first! 🚀🦅

Add comments