frontend / react / shadow_dom_vs_virtual_dom.md

Shadow DOM vs Virtual DOM

3 interview angles 8 min read source

Shadow DOM vs Virtual DOM

Understanding the differences between Shadow DOM and Virtual DOM is crucial for web development, as they serve different purposes in modern web applications.

Table of Contents


What is Shadow DOM?

Shadow DOM is a web standard that provides encapsulation for DOM elements, allowing you to create isolated DOM trees that are separate from the main document’s DOM.

Key Characteristics:

  • Encapsulation: Isolates styles and DOM structure
  • Web Standard: Native browser feature
  • Component Scoping: Prevents style leakage
  • Real DOM: Creates actual DOM nodes

Shadow DOM Structure:

// Shadow DOM creates isolated DOM trees
class MyElement extends HTMLElement {
  constructor() {
    super();

    // Create shadow root
    const shadow = this.attachShadow({ mode: 'open' });

    // Shadow DOM content
    shadow.innerHTML = `
      <style>
        .container { color: red; } /* Scoped to shadow DOM */
      </style>
      <div class="container">
        <h2>Shadow DOM Content</h2>
        <slot></slot>
      </div>
    `;
  }
}

customElements.define('my-element', MyElement);

What is Virtual DOM?

Virtual DOM is a programming concept where a lightweight copy of the actual DOM is kept in memory and synced with the real DOM through a process called reconciliation.

Key Characteristics:

  • Performance Optimization: Minimizes DOM operations
  • Library Implementation: Used by React, Vue, etc.
  • JavaScript Objects: Lightweight representations
  • Reconciliation: Diffing algorithm for updates

Virtual DOM Structure:

// Virtual DOM is JavaScript objects representing DOM
const virtualDOM = {
  type: 'div',
  props: {
    className: 'container',
    children: [
      {
        type: 'h1',
        props: { children: 'Hello World' }
      },
      {
        type: 'p',
        props: { children: 'This is virtual DOM' }
      }
    ]
  }
};

// React converts this to real DOM
function App() {
  return (
    <div className="container">
      <h1>Hello World</h1>
      <p>This is virtual DOM</p>
    </div>
  );
}

Key Differences

1. Purpose:

// Shadow DOM: Encapsulation and isolation
class EncapsulatedComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'closed' });
    shadow.innerHTML = `
      <style>
        /* Styles are scoped to this component only */
        .button { background: blue; }
      </style>
      <button class="button">Click me</button>
    `;
  }
}

// Virtual DOM: Performance optimization
function OptimizedComponent() {
  const [count, setCount] = useState(0);

  // React creates virtual DOM, then reconciles with real DOM
  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}

2. Implementation:

// Shadow DOM: Browser native
class NativeComponent extends HTMLElement {
  connectedCallback() {
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <div class="shadow-content">
        <slot name="header"></slot>
        <slot></slot>
      </div>
    `;
  }
}

// Virtual DOM: JavaScript library
function ReactComponent() {
  // React manages virtual DOM internally
  return (
    <div>
      <header>Header</header>
      <main>Content</main>
    </div>
  );
}

3. Style Isolation:

// Shadow DOM: Automatic style isolation
class StyledComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        /* These styles only affect this component */
        .title { color: red; font-size: 20px; }
        .content { padding: 10px; }
      </style>
      <div>
        <h1 class="title">Title</h1>
        <div class="content">
          <slot></slot>
        </div>
      </div>
    `;
  }
}

// Virtual DOM: No automatic style isolation
function ReactComponent() {
  return (
    <div>
      <h1 className="title">Title</h1>
      <div className="content">Content</div>
    </div>
  );
  // Styles can leak unless using CSS-in-JS or CSS modules
}

4. DOM Access:

// Shadow DOM: Limited access from outside
class ShadowComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'closed' }); // Closed mode
  }
}

const element = new ShadowComponent();
// element.shadowRoot is null (closed mode)
// Cannot access shadow DOM from outside

// Virtual DOM: No direct access (conceptual)
function VirtualComponent() {
  return <div>Content</div>;
}
// Virtual DOM is internal to React
// You work with JSX, not virtual DOM directly

Use Cases

Shadow DOM Use Cases:

// 1. Web Components
class CustomButton extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        button {
          background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
          border: none;
          padding: 12px 24px;
          border-radius: 25px;
          color: white;
          cursor: pointer;
        }
      </style>
      <button><slot></slot></button>
    `;
  }
}

// 2. Third-party widgets
class ThirdPartyWidget extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'closed' });
    // Isolated widget that won't conflict with page styles
  }
}

// 3. Style encapsulation
class EncapsulatedForm extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        input { border: 2px solid blue; }
        button { background: green; }
      </style>
      <form>
        <input type="text" placeholder="Name">
        <button type="submit">Submit</button>
      </form>
    `;
  }
}

Virtual DOM Use Cases:

// 1. Complex UI updates
function ComplexDashboard() {
  const [data, setData] = useState([]);
  const [filters, setFilters] = useState({});
  const [sortBy, setSortBy] = useState('name');

  // Virtual DOM efficiently handles complex re-renders
  return (
    <div>
      <FilterPanel filters={filters} onChange={setFilters} />
      <DataTable data={data} sortBy={sortBy} />
      <Chart data={data} />
    </div>
  );
}

// 2. Frequent updates
function RealTimeCounter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      setCount(c => c + 1); // Virtual DOM batches updates
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return <div>Count: {count}</div>;
}

// 3. Conditional rendering
function ConditionalComponent({ user }) {
  // Virtual DOM efficiently handles conditional rendering
  return (
    <div>
      {user ? (
        <UserProfile user={user} />
      ) : (
        <LoginForm />
      )}
    </div>
  );
}

Implementation Examples

Shadow DOM Implementation:

// Complete Shadow DOM component
class TodoItem extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });

    shadow.innerHTML = `
      <style>
        :host {
          display: block;
          margin: 8px 0;
        }

        .todo-item {
          display: flex;
          align-items: center;
          padding: 12px;
          border: 1px solid #ddd;
          border-radius: 4px;
        }

        .completed {
          text-decoration: line-through;
          opacity: 0.6;
        }

        .checkbox {
          margin-right: 12px;
        }

        .delete-btn {
          margin-left: auto;
          background: #ff4757;
          color: white;
          border: none;
          padding: 4px 8px;
          border-radius: 4px;
          cursor: pointer;
        }
      </style>

      <div class="todo-item">
        <input type="checkbox" class="checkbox">
        <span class="text"><slot></slot></span>
        <button class="delete-btn">Delete</button>
      </div>
    `;

    this.setupEventListeners(shadow);
  }

  setupEventListeners(shadow) {
    const checkbox = shadow.querySelector('.checkbox');
    const text = shadow.querySelector('.text');
    const deleteBtn = shadow.querySelector('.delete-btn');

    checkbox.addEventListener('change', () => {
      text.classList.toggle('completed', checkbox.checked);
    });

    deleteBtn.addEventListener('click', () => {
      this.remove();
    });
  }
}

customElements.define('todo-item', TodoItem);

Virtual DOM Implementation:

// React component using Virtual DOM
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React', completed: false },
    { id: 2, text: 'Build app', completed: false }
  ]);

  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }
        : todo
    ));
  };

  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // React's Virtual DOM efficiently updates only changed items
  return (
    <div className="todo-list">
      {todos.map(todo => (
        <div key={todo.id} className="todo-item">
          <input
            type="checkbox"
            checked={todo.completed}
            onChange={() => toggleTodo(todo.id)}
          />
          <span className={todo.completed ? 'completed' : ''}>
            {todo.text}
          </span>
          <button onClick={() => deleteTodo(todo.id)}>
            Delete
          </button>
        </div>
      ))}
    </div>
  );
}

Best Practices

Shadow DOM Best Practices:

// Good: Use slots for content projection
class GoodComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <div class="container">
        <header><slot name="header">Default Header</slot></header>
        <main><slot>Default Content</slot></main>
        <footer><slot name="footer">Default Footer</slot></footer>
      </div>
    `;
  }
}

// Good: Use CSS custom properties for theming
class ThemedComponent extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    shadow.innerHTML = `
      <style>
        :host {
          --primary-color: #007bff;
          --secondary-color: #6c757d;
        }

        .button {
          background: var(--primary-color);
          color: white;
        }
      </style>
      <button class="button"><slot></slot></button>
    `;
  }
}

// Bad: Don't use closed mode unless necessary
class BadComponent extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'closed' }); // Harder to debug
  }
}

Virtual DOM Best Practices:

// Good: Use keys for lists
function GoodList({ items }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{item.text}</li>
      ))}
    </ul>
  );
}

// Good: Use React.memo for expensive components
const ExpensiveComponent = React.memo(({ data }) => {
  return <div>{/* Expensive rendering */}</div>;
});

// Good: Use stable references
function GoodParent() {
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);

  return <Child onClick={handleClick} />;
}

// Bad: Don't create new objects in render
function BadComponent({ items }) {
  return (
    <div>
      {items.map(item => (
        <Child key={item.id} data={{ ...item }} /> // New object every render
      ))}
    </div>
  );
}

Common Interview Questions

Q: What is the main difference between Shadow DOM and Virtual DOM?

  • Shadow DOM provides encapsulation and style isolation, while Virtual DOM optimizes performance through efficient DOM updates.

Q: When would you use Shadow DOM?

  • For web components, third-party widgets, or when you need style encapsulation and DOM isolation.

Q: When would you use Virtual DOM?

  • For complex UI applications with frequent updates, conditional rendering, or when using frameworks like React.

Q: Can you use both Shadow DOM and Virtual DOM together?

  • Yes, you can use Shadow DOM within React components or create web components that use Virtual DOM internally.

Q: How does Shadow DOM provide style isolation?

  • Shadow DOM creates isolated DOM trees where styles are scoped to the component and don’t leak to or from the main document.

Q: How does Virtual DOM improve performance?

  • By batching DOM updates, minimizing actual DOM operations, and using efficient diffing algorithms.

Q: What are the trade-offs of using Shadow DOM?

  • Limited external access, potential debugging complexity, and browser support considerations.

Q: What are the trade-offs of using Virtual DOM?

  • Memory overhead, learning curve, and potential over-optimization for simple applications.

Summary

  • Shadow DOM provides encapsulation and style isolation for web components
  • Virtual DOM optimizes performance through efficient DOM updates
  • Shadow DOM is a web standard, Virtual DOM is a programming concept
  • Shadow DOM creates real DOM nodes, Virtual DOM uses JavaScript objects
  • Shadow DOM is ideal for reusable components, Virtual DOM for complex applications
  • Both can be used together in modern web development
  • Understanding both is crucial for building scalable web applications

Interview angle

  • “Are shadow DOM and virtual DOM related?” - no, only the names. Shadow DOM is a browser feature giving a real DOM subtree encapsulated style and markup, used by web components. Virtual DOM is React’s in-memory element tree used to compute updates.
  • “Where do they meet?” - using web components inside React. React 19 fixed the long-standing gaps: it now passes non-primitive props as properties rather than stringified attributes, and handles custom events better.
  • “Why would you use shadow DOM?” - genuine style encapsulation with no build step, for widgets embedded in pages you do not control. The cost is that global styles and design tokens do not reach in without explicit parts or CSS custom properties.