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!