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'); // booleanFetch 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:
- The modern browser is incredibly capable
- Web Components allow native componentization
- State management doesn't need a library
- Performance improves dramatically without frameworks
- 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.

