Vanilla JavaScript in 2026: The Return of Pure JS and Why Less is More
Hello HaWkers, an interesting trend is gaining momentum in web development in 2026. Vanilla JavaScript, once dismissed as "too basic," is resurging as a smart choice for developers seeking performance and simplicity.
Let's explore why pure JS is making a comeback and when it makes sense to abandon frameworks.
What Changed
The landscape has changed significantly in recent years:
Factors driving the change:
- Native browser APIs evolved dramatically
- Performance became critical (Core Web Vitals, SEO)
- Framework fatigue reached its peak
- Dependency maintenance costs exploded
- Unnecessary complexity in simple projects
💡 Context: In 2020, using vanilla JS was seen as amateurish. In 2026, it's considered a mature technical decision for many cases.
The Power of Modern JavaScript
Native JavaScript in 2026 is incredibly powerful:
Advanced Native APIs
// Modern APIs that eliminate the need for libraries
// 1. Fetch API - Replaced HTTP libraries
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'HaWker' }),
signal: AbortSignal.timeout(5000), // Native timeout!
});
// 2. Web Components - Components without framework
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
.card { padding: 1rem; border: 1px solid #ddd; }
</style>
<div class="card">
<slot name="name"></slot>
<slot name="email"></slot>
</div>
`;
}
}
customElements.define('user-card', UserCard);
// 3. Intersection Observer - Native lazy loading
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});Modern DOM Manipulation
// DOM manipulation without jQuery or similar
// Powerful selectors
const buttons = document.querySelectorAll('.btn[data-action]');
const form = document.querySelector('#signup-form');
// Efficient event delegation
document.body.addEventListener('click', (e) => {
const button = e.target.closest('[data-action]');
if (!button) return;
const action = button.dataset.action;
handlers[action]?.(e);
});
// Native templates
const template = document.getElementById('item-template');
const clone = template.content.cloneNode(true);
clone.querySelector('.title').textContent = 'New Item';
document.getElementById('list').appendChild(clone);
// ClassList API
element.classList.add('active', 'visible');
element.classList.remove('hidden');
element.classList.toggle('expanded');
element.classList.replace('old-class', 'new-class');
State Without Framework
Managing state without React/Vue is simpler than you think:
Simple Observer Pattern
// Minimalist reactive state system
class Store {
#state = {};
#listeners = new Map();
constructor(initialState = {}) {
this.#state = initialState;
}
getState() {
return structuredClone(this.#state);
}
setState(updates) {
const prevState = this.#state;
this.#state = { ...this.#state, ...updates };
// Notify only affected listeners
for (const [key, callbacks] of this.#listeners) {
if (key in updates && prevState[key] !== updates[key]) {
callbacks.forEach(cb => cb(updates[key], prevState[key]));
}
}
}
subscribe(key, callback) {
if (!this.#listeners.has(key)) {
this.#listeners.set(key, new Set());
}
this.#listeners.get(key).add(callback);
// Return unsubscribe function
return () => this.#listeners.get(key).delete(callback);
}
}
// Usage
const store = new Store({ user: null, cart: [], theme: 'light' });
// Component reacts to changes
store.subscribe('cart', (newCart) => {
document.getElementById('cart-count').textContent = newCart.length;
});
store.subscribe('theme', (theme) => {
document.body.classList.toggle('dark', theme === 'dark');
});
// Update state
store.setState({ cart: [...store.getState().cart, newItem] });Proxy for Reactivity
// Automatic reactivity with Proxy
function createReactive(target, onChange) {
return new Proxy(target, {
set(obj, prop, value) {
const oldValue = obj[prop];
obj[prop] = value;
if (oldValue !== value) {
onChange(prop, value, oldValue);
}
return true;
},
get(obj, prop) {
const value = obj[prop];
// Recursive for nested objects
if (value && typeof value === 'object') {
return createReactive(value, onChange);
}
return value;
}
});
}
// Usage
const state = createReactive(
{ count: 0, user: { name: 'HaWker' } },
(prop, newVal, oldVal) => {
console.log(`${prop} changed from ${oldVal} to ${newVal}`);
updateUI();
}
);
state.count++; // Triggers automatically
state.user.name = 'Dev'; // Also reactive!
Routing Without Library
SPAs are possible with native APIs:
// Minimalist router with History API
class Router {
#routes = new Map();
#notFound = () => {};
constructor() {
window.addEventListener('popstate', () => this.#navigate());
// Intercept links
document.addEventListener('click', (e) => {
const link = e.target.closest('a[data-link]');
if (!link) return;
e.preventDefault();
this.push(link.getAttribute('href'));
});
}
route(path, handler) {
this.#routes.set(path, handler);
return this;
}
notFound(handler) {
this.#notFound = handler;
return this;
}
push(path) {
history.pushState(null, '', path);
this.#navigate();
}
#navigate() {
const path = location.pathname;
// Try exact match
if (this.#routes.has(path)) {
this.#routes.get(path)();
return;
}
// Try match with parameters
for (const [routePath, handler] of this.#routes) {
const params = this.#matchRoute(routePath, path);
if (params) {
handler(params);
return;
}
}
this.#notFound();
}
#matchRoute(routePath, actualPath) {
const routeParts = routePath.split('/');
const actualParts = actualPath.split('/');
if (routeParts.length !== actualParts.length) return null;
const params = {};
for (let i = 0; i < routeParts.length; i++) {
if (routeParts[i].startsWith(':')) {
params[routeParts[i].slice(1)] = actualParts[i];
} else if (routeParts[i] !== actualParts[i]) {
return null;
}
}
return params;
}
start() {
this.#navigate();
return this;
}
}
// Usage
const router = new Router()
.route('/', () => renderHome())
.route('/blog', () => renderBlog())
.route('/blog/:slug', ({ slug }) => renderPost(slug))
.route('/user/:id/posts', ({ id }) => renderUserPosts(id))
.notFound(() => render404())
.start();
Bundle Size Comparison
The impact on bundle size is dramatic:
| Approach | Bundle Size | Parse Time |
|---|---|---|
| React + Router + State | ~150KB gzip | ~200ms |
| Vue 3 + Router + Pinia | ~80KB gzip | ~120ms |
| Vanilla JS (equivalent) | ~5KB gzip | ~10ms |
Real Performance Impact
// Measuring real impact
const performanceComparison = {
// Time to First Contentful Paint
fcp: {
react: '1.2s - 2.5s',
vue: '0.8s - 1.8s',
vanilla: '0.3s - 0.6s'
},
// Time to Interactive
tti: {
react: '2.5s - 4.0s',
vue: '1.5s - 3.0s',
vanilla: '0.5s - 1.0s'
},
// JavaScript execution time
jsExecution: {
react: '150ms - 400ms',
vue: '80ms - 200ms',
vanilla: '10ms - 50ms'
},
// Memory footprint
memory: {
react: '15MB - 30MB',
vue: '10MB - 20MB',
vanilla: '3MB - 8MB'
}
};
When to Use Vanilla JS
Ideal Cases
// Projects where vanilla JS shines
const idealCases = {
// Landing pages and static sites
staticSites: {
reason: 'No need for complex reactivity',
benefit: 'Maximum performance, better SEO',
examples: ['Portfolio', 'Corporate', 'Landing page']
},
// Widgets and isolated components
widgets: {
reason: 'Single component doesn\'t justify framework',
benefit: 'Minimal bundle, easy integration',
examples: ['Chat widget', 'Embed form', 'Custom player']
},
// Libraries and plugins
libraries: {
reason: 'Shouldn\'t force dependencies on users',
benefit: 'Framework-agnostic, smaller footprint',
examples: ['SDK', 'Analytics', 'UI components']
},
// Projects with performance requirements
performance: {
reason: 'Every kilobyte matters',
benefit: 'Optimized Core Web Vitals',
examples: ['E-commerce', 'News sites', 'PWAs']
},
// Long-lived applications
longevity: {
reason: 'Fewer dependencies = fewer breaking changes',
benefit: 'Simplified maintenance for years',
examples: ['Internal systems', 'Enterprise tools']
}
};Cases Where Framework Still Makes Sense
// When frameworks are justified
const frameworkCases = {
// Complex apps with lots of state
complexState: {
reason: 'Shared state between dozens of components',
recommendation: 'React, Vue, Svelte'
},
// Large teams
largeTeams: {
reason: 'Conventions and structure help coordination',
recommendation: 'Angular, Next.js'
},
// Ecosystem needed
ecosystem: {
reason: 'Needs framework-specific libraries',
recommendation: 'Choose by ecosystem'
},
// Rapid prototyping
prototyping: {
reason: 'Development speed prioritized',
recommendation: 'Vue, Svelte'
}
};
Tools for Vanilla JS in 2026
Minimalist Build Tools
// Modern configuration for vanilla JS
// esbuild - Ultra-fast build
// esbuild.config.mjs
import * as esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/main.js'],
bundle: true,
minify: true,
sourcemap: true,
target: ['es2022'],
outfile: 'dist/bundle.js',
// No unnecessary transpilation for modern browsers
format: 'esm',
});
// Vite for development (without framework)
// vite.config.js
export default {
build: {
target: 'esnext',
minify: 'esbuild',
rollupOptions: {
input: 'src/main.js',
},
},
// Hot reload works with vanilla JS!
};Testing Without Jest
// Native tests with Node.js test runner
import { test, describe, beforeEach } from 'node:test';
import assert from 'node:assert';
import { Store } from './store.js';
describe('Store', () => {
let store;
beforeEach(() => {
store = new Store({ count: 0 });
});
test('should initialize with state', () => {
assert.deepStrictEqual(store.getState(), { count: 0 });
});
test('should update state', () => {
store.setState({ count: 5 });
assert.strictEqual(store.getState().count, 5);
});
test('should notify subscribers', (t) => {
const callback = t.mock.fn();
store.subscribe('count', callback);
store.setState({ count: 10 });
assert.strictEqual(callback.mock.calls.length, 1);
assert.deepStrictEqual(callback.mock.calls[0].arguments, [10, 0]);
});
});
// Run: node --test
Gradual Migration
If you want to experiment, start gradually:
Migration Strategy
// Incremental approach
const migrationStrategy = {
phase1: {
action: 'Audit dependencies',
tasks: [
'List all libraries used',
'Identify what can be native',
'Calculate potential savings'
]
},
phase2: {
action: 'Replace utilities',
tasks: [
'Remove Lodash (use native methods)',
'Remove Axios (use Fetch)',
'Remove Moment (use Intl, Temporal)'
]
},
phase3: {
action: 'Isolate components',
tasks: [
'Create Web Components for UI',
'Move logic to ES modules',
'Reduce framework coupling'
]
},
phase4: {
action: 'Evaluate framework',
tasks: [
'Measure real state complexity',
'Consider if framework is necessary',
'Migrate if benefit is clear'
]
}
};Example: Replacing Lodash
// Before: Lodash (70KB)
import _ from 'lodash';
const unique = _.uniq(array);
const grouped = _.groupBy(items, 'category');
const debounced = _.debounce(fn, 300);
// After: Native (0KB)
const unique = [...new Set(array)];
const grouped = Object.groupBy(items, item => item.category);
// Or for older browsers:
const grouped = items.reduce((acc, item) => {
(acc[item.category] ??= []).push(item);
return acc;
}, {});
const debounced = (fn, delay) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), delay);
};
};
The Future
Trends for 2026-2027
What to expect:
- More native browser APIs
- Web Components more adopted
- Smaller and more focused frameworks
- "Islands Architecture" mainstream
- Ahead-of-time compilation dominant
For Developers
Recommendations:
- Learn JavaScript deeply, not just frameworks
- Understand native browser APIs
- Evaluate real needs before adding dependencies
- Performance should be a design consideration, not post-optimization
Conclusion
The return of Vanilla JavaScript in 2026 doesn't mean frameworks died. It means we have more options and maturity to choose the right tool for each job.
For simple projects, static sites, widgets, and situations where performance is critical, vanilla JS is often the best choice. For complex applications with lots of shared state and large teams, frameworks still have their place.
The important thing is making conscious choices instead of blindly following trends.
If you want to understand more about development trends, I recommend checking out another article: TypeScript Is the Standard in 2026 where you'll discover how TypeScript dominated the ecosystem.

