JS frameworks be like: “I see you clicked. Let me rerender your whole soul.”
We need to talk. While everyone’s out there building apps that could power a small rocket ship just to display a todo list, I’m over here having a love affair with document.querySelector. Yeah, that’s right – the same DOM API that’s been sitting in your browser since Obama was president (the first time).
The Framework Fever Dream
Don’t get me wrong, I love React, Vue, and all the cool kids on the block. But sometimes I feel like we’ve collectively forgotten that browsers got really good at DOM manipulation. Like, scary good. Modern browsers are basically Formula 1 cars, and we’re strapping a whole trailer to them because we’re afraid to drive stick.
Here’s what typically happens in framework-land:
// User clicks button
// Framework: "HOLD EVERYTHING! 🚨"
// Framework: "Let me diff the entire virtual DOM..."
// Framework: "Now let me reconcile with the real DOM..."
// Framework: "Updating component tree..."
// Framework: "Triggering 17 lifecycle methods..."
// Button finally changes color after 50ms
Meanwhile, vanilla JS is over here like:
document.querySelector('#my-button').style.backgroundColor = 'blue';
// Done. That's it. 0.001ms later, button is blue.
The Beauty of Direct DOM Manipulation
Let me show you something beautiful. Here’s how you build a dynamic, interactive widget without a single framework:
class TodoWidget {
constructor(container) {
this.container = document.querySelector(container);
this.todos = [];
this.render();
this.attachEventListeners();
}
render() {
this.container.innerHTML = `
<div class="todo-widget">
<input type="text" class="todo-input" placeholder="What needs doing?">
<button class="add-btn">Add</button>
<ul class="todo-list"></ul>
</div>
`;
this.updateTodoList();
}
updateTodoList() {
const list = this.container.querySelector('.todo-list');
list.innerHTML = this.todos.map((todo, index) => `
<li class="todo-item ${todo.completed ? 'completed' : ''}">
<span class="todo-text">${todo.text}</span>
<button class="complete-btn" data-index="${index}">
${todo.completed ? '✓' : 'Mark Done'}
</button>
<button class="delete-btn" data-index="${index}">Delete</button>
</li>
`).join('');
}
attachEventListeners() {
const input = this.container.querySelector('.todo-input');
const addBtn = this.container.querySelector('.add-btn');
const list = this.container.querySelector('.todo-list');
addBtn.addEventListener('click', () => this.addTodo(input.value));
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.addTodo(input.value);
});
// Event delegation for dynamic buttons
list.addEventListener('click', (e) => {
const index = parseInt(e.target.dataset.index);
if (e.target.classList.contains('complete-btn')) {
this.toggleComplete(index);
} else if (e.target.classList.contains('delete-btn')) {
this.deleteTodo(index);
}
});
}
addTodo(text) {
if (text.trim()) {
this.todos.push({ text: text.trim(), completed: false });
this.updateTodoList();
this.container.querySelector('.todo-input').value = '';
}
}
toggleComplete(index) {
this.todos[index].completed = !this.todos[index].completed;
this.updateTodoList();
}
deleteTodo(index) {
this.todos.splice(index, 1);
this.updateTodoList();
}
}
// Initialize it
new TodoWidget('#todo-container');
Look at that! A fully functional todo app in vanilla JavaScript. No build step, no virtual DOM, no 500MB node_modules folder. Just you, the browser, and some good old-fashioned DOM manipulation.
Why Modern DOM APIs Are Actually Amazing
Here’s the thing everyone forgot while we were busy framework-hopping: browser APIs got insanely powerful. Check out what you can do:
// Query selection that makes jQuery weep
const buttons = document.querySelectorAll('button:not([disabled])');
const lastChild = document.querySelector('ul li:last-child');
const evenRows = document.querySelectorAll('tr:nth-child(even)');
// Animation that's smoother than butter
const modal = document.querySelector('.modal');
modal.animate([
{ opacity: 0, transform: 'scale(0.8)' },
{ opacity: 1, transform: 'scale(1)' }
], {
duration: 200,
easing: 'ease-out'
});
// Intersection Observer for lazy loading (goodbye scroll listeners!)
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadContent(entry.target);
}
});
});
// Resize Observer for responsive components
const resizeObserver = new ResizeObserver(entries => {
entries.forEach(entry => {
if (entry.contentRect.width < 600) {
entry.target.classList.add('mobile-layout');
}
});
});
The Performance Reality Check
Let’s talk numbers because I love a good benchmark fight:
- Direct DOM manipulation: ~0.1ms to update an element
- React setState: ~16ms+ for a simple update (with reconciliation)
- Vue reactive update: ~8-12ms depending on component complexity
Now, I’m not saying frameworks are slow – they’re doing a lot of smart optimizations behind the scenes. But sometimes you just want to change a button color, and you don’t need the entire component lifecycle to throw a parade about it.
// This is all you need for most DOM updates
function updateStatus(element, status) {
element.textContent = status;
element.className = `status ${status.toLowerCase()}`;
// Want animation? Sure!
element.animate([
{ backgroundColor: '#fff3cd' },
{ backgroundColor: 'transparent' }
], 300);
}
// Call it directly on any element
updateStatus(document.querySelector('#server-status'), 'Online');
When Vanilla JS Actually Shines
Don’t get me wrong – I’m not suggesting you rewrite your entire React app in vanilla JS (please don’t). But there are sweet spots where the good old DOM APIs are perfect:
- Widgets and plugins that need to integrate anywhere
- Performance-critical animations where every millisecond counts
- Simple interactivity that doesn’t need state management
- Legacy system integration where you can’t introduce build tools
- Learning and prototyping where you want to understand what’s really happening
The Bottom Line
Modern browsers are incredible. They’ve got APIs for everything, performance that would make 2010-you cry tears of joy, and DOM manipulation that’s so smooth it’s basically butter.
Yes, frameworks solve real problems – state management, component reusability, team collaboration. But next time you reach for that 100KB framework to add a click handler, maybe… just maybe… give document.querySelector a chance to show off.
After all, sometimes the real MVP was inside the browser all along. 🏆
P.S. - If you do decide to go vanilla, just remember: with great power comes great responsibility to not write jQuery-style spaghetti code. Keep it clean, keep it organized, and for the love of all that’s holy, use classes and modules!