diff --git a/packages/demo/meta.config.js b/packages/demo/meta.config.js
index f329eb864..03ee68e3e 100644
--- a/packages/demo/meta.config.js
+++ b/packages/demo/meta.config.js
@@ -4,6 +4,9 @@ import { prototyping } from '@studiometa/webpack-config-preset-prototyping';
 
 export default defineConfig({
   presets: [prototyping({ ts: true })],
+  server(config) {
+    config.snippet = false;
+  },
   webpack(config) {
     config.resolve.alias = {
       ...config.resolve.alias,
diff --git a/packages/demo/src/js/app.ts b/packages/demo/src/js/app.ts
index f7929cf7f..7c4277541 100644
--- a/packages/demo/src/js/app.ts
+++ b/packages/demo/src/js/app.ts
@@ -10,6 +10,7 @@ import {
   BaseConfig,
   withDrag,
   withName,
+  getInstances,
 } from '@studiometa/js-toolkit';
 import { matrix } from '@studiometa/js-toolkit/utils';
 import ScrollToDemo from './components/ScrollToDemo.js';
@@ -23,58 +24,6 @@ import ScrolledInViewOffset from './components/ScrolledInViewOffset.js';
 import MediaQueryDemo from './components/MediaQueryDemo.js';
 import PointerProps from './components/PointerProps.js';
 
-let numberOfTick = 0;
-let time = performance.now();
-const interval = setInterval(() => {
-  const newTime = performance.now();
-  numberOfTick += 1;
-  console.log('#%d blocking time: %d ms', numberOfTick, newTime - time);
-  time = newTime;
-
-  if (numberOfTick > total * 2) {
-    clearInterval(interval);
-  }
-}, 0);
-
-let count = 0;
-const total = 66;
-
-function getDeepNestedComponentName(index) {
-  return `TestDeepNested${index}`;
-}
-
-function makeDeepNestedComponent(index) {
-  return class extends withExtraConfig(Base, {
-    name: getDeepNestedComponentName(index),
-    components: {},
-  }) {
-    mounted() {
-      // console.log(this.$id);
-    }
-  };
-}
-
-const TestDeepNested = makeDeepNestedComponent(0);
-let CurrentClass = TestDeepNested;
-while (count < total) {
-  count += 1;
-  const NewClass = makeDeepNestedComponent(count);
-  if (CurrentClass.config.components) {
-    CurrentClass.config.components[NewClass.config.name] = NewClass;
-  }
-
-  CurrentClass = NewClass;
-}
-
-const TestManyInstance = class extends withExtraConfig(Base, {
-  name: 'TestManyInstance',
-  debug: false,
-}) {
-  mounted() {
-    // console.log(this.$id);
-  }
-};
-
 /**
  * App class.
  */
@@ -100,8 +49,8 @@ class App extends Base {
       AnimateTestMultiple,
       ResponsiveOptions,
       ScrolledInViewOffset,
-      TestDeepNested,
-      TestManyInstance,
+      // TestDeepNested,
+      // TestManyInstance,
       Accordion: (app) =>
         importWhenVisible(
           async () => {
@@ -185,6 +134,7 @@ class App extends Base {
    * @inheritdoc
    */
   mounted() {
+    window.APP = this;
     this.$log('Mounted 🎉');
   }
 
@@ -200,13 +150,9 @@ class App extends Base {
     this.$log('resized', props);
   }
 
-  onDocumentClick(event) {
-    console.log('onDocumentClick', event);
-  }
-
   onWindowResize(event) {
     console.log('onWindowResize', event);
   }
 }
 
-export default createApp(App);
+export default createApp(App, { root: document.querySelector('main') });
diff --git a/packages/demo/src/js/components/ParentNativeEvent/Child.js b/packages/demo/src/js/components/ParentNativeEvent/Child.js
index 66cd833ea..eaffb5dea 100644
--- a/packages/demo/src/js/components/ParentNativeEvent/Child.js
+++ b/packages/demo/src/js/components/ParentNativeEvent/Child.js
@@ -9,5 +9,28 @@ export default class Child extends Base {
    */
   static config = {
     name: 'Child',
+    log: true,
+    debug: false,
   };
+
+  msg(...msg) {
+    this.$el.textContent = `[${this.$id}] ${msg.join(' ')}`;
+  }
+
+  mounted() {
+    this.msg('Mounted', performance.now());
+  }
+
+  updated() {
+    this.msg('Updated', performance.now());
+  }
+
+  destroyed() {
+    this.$log('destroyed');
+  }
+
+  terminated() {
+    this.$log('terminated');
+    this.msg('Terminated', performance.now());
+  }
 }
diff --git a/packages/demo/src/js/components/ParentNativeEvent/index.js b/packages/demo/src/js/components/ParentNativeEvent/index.js
index 54f489f23..b12ec3442 100644
--- a/packages/demo/src/js/components/ParentNativeEvent/index.js
+++ b/packages/demo/src/js/components/ParentNativeEvent/index.js
@@ -1,4 +1,5 @@
 import { Base } from '@studiometa/js-toolkit';
+import { createElement } from '@studiometa/js-toolkit/utils';
 import Child from './Child.js';
 
 /**
@@ -11,22 +12,27 @@ export default class ParentNativeEvent extends Base {
   static config = {
     name: 'ParentNativeEvent',
     log: true,
+    debug: false,
     components: {
       Child,
     },
   };
 
-  /**
-   *
-   */
-  onChildClick(...args) {
-    this.$log(this.$id, 'onChildClick', ...args);
+  updated() {
+    this.$log(this.$children.Child);
   }
 
-  /**
-   *
-   */
-  onChildDede(...args) {
-    this.$log(this.$id, 'onChildDede', ...args);
+  onDocumentClick({ event }) {
+    if (event.metaKey) {
+      this.$el.firstElementChild.remove();
+    } else if (event.altKey) {
+      this.$children.Child[0].$el.dataset.component = '';
+    } else {
+      this.$el.append(
+        createElement('button', {
+          dataComponent: 'Child',
+        }),
+      );
+    }
   }
 }
diff --git a/packages/demo/src/templates/pages/child-native-event.twig b/packages/demo/src/templates/pages/child-native-event.twig
index fd0192b98..464141ccf 100644
--- a/packages/demo/src/templates/pages/child-native-event.twig
+++ b/packages/demo/src/templates/pages/child-native-event.twig
@@ -1,7 +1,6 @@
 {% extends '@layouts/base.twig' %}
 {% block main %}
-  
-    
+  
     
   
 {% endblock %}
diff --git a/packages/js-toolkit/Base/Base.ts b/packages/js-toolkit/Base/Base.ts
index c5e30292a..c074b688b 100644
--- a/packages/js-toolkit/Base/Base.ts
+++ b/packages/js-toolkit/Base/Base.ts
@@ -1,10 +1,10 @@
-/* eslint-disable no-use-before-define */
 import {
   getComponentElements,
   getEventTarget,
   addToQueue,
   addInstance,
   deleteInstance,
+  getInstances,
 } from './utils.js';
 import {
   ChildrenManager,
@@ -13,7 +13,9 @@ import {
   EventsManager,
   OptionsManager,
 } from './managers/index.js';
+import { useMutation } from '../services/MutationService.js';
 import { noop, isDev, isFunction, isArray } from '../utils/index.js';
+import { features } from './features.js';
 
 let id = 0;
 
@@ -74,6 +76,8 @@ export class Base
 {
    */
   static readonly $isBase = true as const;
 
+  static __mutationSymbol = Symbol('mutation');
+
   /**
    * The instance parent.
    */
@@ -312,6 +316,68 @@ export class Base {
       this[`__${service.toLowerCase()}`] = new this.__managers[`${service}Manager`](this);
     }
 
+    const service = useMutation(document.querySelector('main'), {
+      childList: true,
+      subtree: true,
+      attributes: true,
+      attributeFilter: [features.get('attributes').component],
+    });
+    const key = this.constructor.__mutationSymbol;
+    if (!service.has(key)) {
+      service.add(key, (props) => {
+        const actions = new Map any>();
+        const instances = getInstances();
+
+        for (const mutation of props.mutations) {
+          // Update parent instance when an instance node has been removed from the DOM.
+          for (const node of mutation.removedNodes) {
+            if (node.nodeType !== Node.ELEMENT_NODE) continue;
+
+            for (const instance of instances) {
+              if (actions.has(instance.$parent ?? instance)) continue;
+              if (instance.$el.isConnected) continue;
+              if (!node.contains(instance.$el)) continue;
+
+              actions.set(instance.$parent ?? instance, () => {
+                if (instance.$parent) {
+                  instance.$update();
+                } else {
+                  instance.$destroy();
+                }
+              });
+            }
+          }
+
+          // Update instances whose child DOM has changed
+          for (const node of mutation.addedNodes) {
+            if (node.nodeType !== Node.ELEMENT_NODE) continue;
+            if (actions.has(node)) continue;
+
+            for (const instance of instances) {
+              if (instance.$el.contains(node)) {
+                actions.set(node, () => instance.$update());
+              }
+            }
+          }
+
+          // Update instances when a data-component attribute has changed
+          if (mutation.type === 'attributes') {
+            for (const instance of instances) {
+              if (actions.has(instance)) continue;
+
+              if (instance.$el.contains(mutation.target)) {
+                actions.set(instance, () => instance.$update());
+              }
+            }
+          }
+        }
+
+        for (const update of actions.values()) {
+          update();
+        }
+      });
+    }
+
     this.$on('mounted', () => {
       addInstance(this);
     });
diff --git a/packages/js-toolkit/Base/managers/ChildrenManager.ts b/packages/js-toolkit/Base/managers/ChildrenManager.ts
index 8481de8bf..c9cca9110 100644
--- a/packages/js-toolkit/Base/managers/ChildrenManager.ts
+++ b/packages/js-toolkit/Base/managers/ChildrenManager.ts
@@ -5,7 +5,9 @@ import { getComponentElements, addToQueue } from '../utils.js';
 /**
  * Children manager.
  */
-export class ChildrenManager extends AbstractManager {
+export class ChildrenManager<
+  T extends Record[]>,
+> extends AbstractManager {
   /**
    * Store async component promises to avoid calling them multiple times and
    * waiting for them when they are already resolved.
@@ -27,9 +29,32 @@ export class ChildrenManager extends AbstractManager {
    * Register instances of all children components.
    */
   async registerAll() {
+    const previousProps = { ...this.__children };
+
     for (const [name, component] of Object.entries(this.__config.components)) {
       this.__register(name, component);
     }
+
+    let childrenToDestroy = new Set();
+    for (const [previousName, previousChildren] of Object.entries(previousProps)) {
+      if (!this.props[previousName]) {
+        childrenToDestroy = childrenToDestroy.union(new Set(previousChildren));
+        continue;
+      }
+
+      const previousChildrenSet = new Set(previousChildren);
+      const childrenSet = new Set(this.props[previousName]);
+      const diff = previousChildrenSet.difference(childrenSet);
+      childrenToDestroy = childrenToDestroy.union(diff);
+    }
+
+    for (const child of childrenToDestroy) {
+      if (child instanceof Promise) {
+        child.then(instance => instance.$destroy())
+      } else {
+        child.$destroy();
+      }
+    }
   }
 
   /**
@@ -78,6 +103,8 @@ export class ChildrenManager extends AbstractManager {
     await instance[hook]();
   }
 
+  __children: T = {} as T;
+
   /**
    * Register instance of a child component.
    *
@@ -114,6 +141,12 @@ export class ChildrenManager extends AbstractManager {
       }
     }
 
+    Object.defineProperty(this.__children, name, {
+      enumerable: true,
+      configurable: true,
+      value: children,
+    });
+
     return children;
   }
 
diff --git a/packages/js-toolkit/helpers/createApp.ts b/packages/js-toolkit/helpers/createApp.ts
index dc855448e..588afbe7b 100644
--- a/packages/js-toolkit/helpers/createApp.ts
+++ b/packages/js-toolkit/helpers/createApp.ts
@@ -1,8 +1,6 @@
 import type { Base, BaseConstructor, BaseProps } from '../Base/index.js';
 import type { Features } from '../Base/features.js';
-import { getInstances } from '../Base/index.js';
 import { features } from '../Base/features.js';
-import { useMutation } from '../services/index.js';
 import { isBoolean, isObject, isString } from '../utils/index.js';
 
 export type CreateAppOptions = Partial & {
@@ -42,25 +40,6 @@ export function createApp, T extends BaseProps =
     features.set('attributes', attributes);
   }
 
-  // Terminate components whose root element has been removed from the DOM
-  const service = useMutation(document.documentElement, { childList: true, subtree: true });
-  const symbol = Symbol('createApp');
-  service.add(symbol, (props) => {
-    for (const mutation of props.mutations) {
-      if (mutation.type === 'childList') {
-        for (const node of mutation.removedNodes) {
-          if (!node.isConnected) {
-            for (const instance of getInstances()) {
-              if (node === instance.$el || node.contains(instance.$el)) {
-                instance.$terminate();
-              }
-            }
-          }
-        }
-      }
-    }
-  });
-
   async function init() {
     app = new App(root) as S & Base;
     await app.$mount();