AvnishYadav
WorkProjectsBlogsNewsletterSupportAbout
Work With Me

Avnish Yadav

Engineer. Automate. Build. Scale.

© 2026 Avnish Yadav. All rights reserved.

The Automation Update

AI agents, automation, and micro-SaaS. Weekly.

Explore

  • Home
  • Projects
  • Blogs
  • Newsletter Archive
  • About
  • Contact
  • Support

Legal

  • Privacy Policy

Connect

LinkedInGitHubInstagramYouTube
Beyond Frameworks: Practical DOM Manipulation Techniques for High-Performance UIs
2026-02-21

Beyond Frameworks: Practical DOM Manipulation Techniques for High-Performance UIs

7 min readDevelopmentBuildsDOM ManipulationFrontend DevelopmentJavaScriptPerformanceWeb Engineering

Stop relying solely on abstractions. Learn how to manipulate the DOM efficiently using DocumentFragments, Event Delegation, and proper State Management by building a raw, high-performance To-Do application.

The DOM is not a Black Box

In the age of React, Vue, and Svelte, it is easy to forget that at the bottom of the stack, we are still just manipulating the Document Object Model (DOM). I interview developers often who can build complex useEffect chains but stumble when asked to optimize a list rendering without a virtual DOM.

Understanding how the browser paints, reflows, and handles events is what separates a library user from a software engineer. When your framework optimizes poorly, you need to know what's happening under the hood to fix it.

Today, we are going to build a functional, interactive To-Do list using Vanilla JavaScript. But this isn't a beginner tutorial. We are going to build it using techniques that prioritize memory efficiency and rendering performance. We aren't just hacking it together; we are engineering it.

1. Efficient Selection: Cache Your Elements

The first mistake I see in raw DOM scripts is redundant querying. Searching the DOM tree is expensive. Every time you run document.querySelector, the browser has to traverse the tree to find your node.

In our To-Do app, we need references to the input field, the add button, and the list container. Do not query these inside your functions.

// ❌ BAD: Querying every time a function runs
function checkInput() {
    const input = document.querySelector('.todo-input');
    // logic...
}

// ✅ GOOD: Cache references once
const elements = {
    input: document.querySelector('.todo-input'),
    list: document.querySelector('.todo-list'),
    form: document.querySelector('.todo-form')
};

By storing these in an object at the top level, we traverse the DOM once on load, and never again for these static elements.

2. The Performance Killer: Reflow and Repaint

When you modify the DOM (add a class, change size, append a child), the browser has to calculate the geometry of the page (Reflow) and then draw the pixels (Repaint). This is computationally heavy.

If you are adding 100 to-do items from an API, and you append them one by one, you trigger the layout engine 100 times.

The Solution: DocumentFragment

A DocumentFragment is a lightweight container. It lives in memory, not the DOM tree. You can append 100 items to the fragment, and the browser doesn't calculate a single pixel. Then, you append the fragment to the DOM, triggering only one reflow.

Here is how we handle the initial render of our To-Do list:

const initialTodos = ['Build System', 'Deploy Agent', 'Optimize DB'];

function renderBatch(todos) {
    // Create the fragment in memory
    const fragment = document.createDocumentFragment();

    todos.forEach(todo => {
        const li = document.createElement('li');
        li.className = 'todo-item';
        li.textContent = todo;
        fragment.appendChild(li);
    });

    // One single interaction with the live DOM
    elements.list.appendChild(fragment);
}

3. Event Delegation: Memory Management 101

This is the most critical pattern for interactive lists. If you have 1,000 list items, and you want them to be deletable or clickable, a novice approach is to attach an addEventListener to every single <li>.

This is a memory leak waiting to happen. It creates thousands of event listeners. Furthermore, if you dynamically add a new item later, it won't have a listener attached unless you manually add it during creation.

Event Delegation leverages the concept of "Event Bubbling." Events bubble up from the target element to the parent. We attach one listener to the parent (<ul>), and catch events as they bubble up.

// Attach listener to the parent UL only
elements.list.addEventListener('click', (e) => {
    // Identify what was actually clicked
    const target = e.target;

    // Handle Delete Button
    if (target.matches('.delete-btn')) {
        const item = target.closest('.todo-item');
        deleteTodo(item.dataset.id);
        item.remove();
        return;
    }

    // Handle Completion Toggle
    if (target.matches('.todo-checkbox')) {
        const item = target.closest('.todo-item');
        toggleComplete(item.dataset.id);
    }
});

Note the use of closest(). This is a robust method. Even if the user clicks an icon inside the delete button, closest('.todo-item') ensures we grab the correct parent container to act upon.

4. Data Attributes for State Linking

The DOM should be a reflection of state, not the state storage itself. However, we need a way to link a specific DOM element to our JavaScript data. This is where data-attributes shine.

In our system, every To-Do item in the array has a unique ID (UUID or timestamp). When rendering, we attach this to the DOM element.

function createTodoElement(todoObj) {
    const li = document.createElement('li');
    // Storing the ID in the DOM for retrieval later
    li.dataset.id = todoObj.id; 
    li.innerHTML = `
        <span>${sanitize(todoObj.text)}</span>
        <button class="delete-btn">Delete</button>
    `;
    return li;
}

When the event delegation listener triggers (from the previous section), we access item.dataset.id to know exactly which object in our JavaScript array to update.

5. Security: Sanitization against XSS

When using vanilla JS, developers often reach for innerHTML because it is faster to write than createElement. However, if you are rendering user input directly into innerHTML, you are vulnerable to Cross-Site Scripting (XSS).

If a user enters <img src=x onerror=alert(1)> as a to-do item, and you inject that, the script executes.

Always sanitize. For a simple app, using textContent is the safest route because it treats input as raw text, not HTML.

// ❌ RISKY
li.innerHTML = todoInput.value;

// ✅ SAFE
li.textContent = todoInput.value;

If you must use innerHTML for complex templates, use a sanitization library or build elements node-by-node.

6. Putting it Together: The Architecture

To keep this maintainable, we separate our logic into State and UI.

The State:

let state = {
    todos: []
};

function addTodo(text) {
    const newTodo = { id: Date.now(), text, completed: false };
    state.todos.push(newTodo);
    // Update UI
    ui.appendItem(newTodo);
}

The UI:

const ui = {
    appendItem: (todo) => {
        const element = createTodoElement(todo);
        elements.list.appendChild(element);
    },
    removeElement: (id) => {
        const el = document.querySelector(`[data-id="${id}"]`);
        if (el) el.remove();
    }
};

Conclusion

Frameworks are powerful tools for managing complexity, but they come with overhead. For micro-SaaS widgets, browser extensions, or lightweight interactive agents, shipping a 200KB React bundle is often engineering malpractice.

By mastering DocumentFragment, Event Delegation, and proper DOM caching, you can build interfaces that run at 60FPS on low-end devices without a single dependency. Build closer to the metal, and your systems will be more resilient.

Share

Comments

Loading comments...

Add a comment

By posting a comment, you’ll be subscribed to the newsletter. You can unsubscribe anytime.

0/2000