diff --git a/packages/lexical-code/src/CodeHighlighter.ts b/packages/lexical-code/src/CodeHighlighter.ts
index 329e2ed19a2..e4a4f626f28 100644
--- a/packages/lexical-code/src/CodeHighlighter.ts
+++ b/packages/lexical-code/src/CodeHighlighter.ts
@@ -818,18 +818,22 @@ export function registerCodeHighlighting(
   }
 
   return mergeRegister(
-    editor.registerMutationListener(CodeNode, (mutations) => {
-      editor.update(() => {
-        for (const [key, type] of mutations) {
-          if (type !== 'destroyed') {
-            const node = $getNodeByKey(key);
-            if (node !== null) {
-              updateCodeGutter(node as CodeNode, editor);
+    editor.registerMutationListener(
+      CodeNode,
+      (mutations) => {
+        editor.update(() => {
+          for (const [key, type] of mutations) {
+            if (type !== 'destroyed') {
+              const node = $getNodeByKey(key);
+              if (node !== null) {
+                updateCodeGutter(node as CodeNode, editor);
+              }
             }
           }
-        }
-      });
-    }),
+        });
+      },
+      {skipInitialization: false},
+    ),
     editor.registerNodeTransform(CodeNode, (node) =>
       codeNodeTransform(node, editor, tokenizer as Tokenizer),
     ),
diff --git a/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx
index 53809f04cda..b200b279e69 100644
--- a/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx
+++ b/packages/lexical-playground/src/plugins/CodeActionMenuPlugin/index.tsx
@@ -109,26 +109,32 @@ function CodeActionMenuContainer({
     };
   }, [shouldListenMouseMove, debouncedOnMouseMove]);
 
-  editor.registerMutationListener(CodeNode, (mutations) => {
-    editor.getEditorState().read(() => {
-      for (const [key, type] of mutations) {
-        switch (type) {
-          case 'created':
-            codeSetRef.current.add(key);
-            setShouldListenMouseMove(codeSetRef.current.size > 0);
-            break;
-
-          case 'destroyed':
-            codeSetRef.current.delete(key);
-            setShouldListenMouseMove(codeSetRef.current.size > 0);
-            break;
-
-          default:
-            break;
-        }
-      }
-    });
-  });
+  useEffect(() => {
+    return editor.registerMutationListener(
+      CodeNode,
+      (mutations) => {
+        editor.getEditorState().read(() => {
+          for (const [key, type] of mutations) {
+            switch (type) {
+              case 'created':
+                codeSetRef.current.add(key);
+                break;
+
+              case 'destroyed':
+                codeSetRef.current.delete(key);
+                break;
+
+              default:
+                break;
+            }
+          }
+        });
+        setShouldListenMouseMove(codeSetRef.current.size > 0);
+      },
+      {skipInitialization: false},
+    );
+  }, [editor]);
+
   const normalizedLang = normalizeCodeLang(lang);
   const codeFriendlyName = getLanguageFriendlyName(lang);
 
diff --git a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx
index 66915aacf01..1fc15288d41 100644
--- a/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx
+++ b/packages/lexical-playground/src/plugins/CommentPlugin/index.tsx
@@ -840,43 +840,47 @@ export default function CommentPlugin({
           });
         },
       ),
-      editor.registerMutationListener(MarkNode, (mutations) => {
-        editor.getEditorState().read(() => {
-          for (const [key, mutation] of mutations) {
-            const node: null | MarkNode = $getNodeByKey(key);
-            let ids: NodeKey[] = [];
-
-            if (mutation === 'destroyed') {
-              ids = markNodeKeysToIDs.get(key) || [];
-            } else if ($isMarkNode(node)) {
-              ids = node.getIDs();
-            }
-
-            for (let i = 0; i < ids.length; i++) {
-              const id = ids[i];
-              let markNodeKeys = markNodeMap.get(id);
-              markNodeKeysToIDs.set(key, ids);
+      editor.registerMutationListener(
+        MarkNode,
+        (mutations) => {
+          editor.getEditorState().read(() => {
+            for (const [key, mutation] of mutations) {
+              const node: null | MarkNode = $getNodeByKey(key);
+              let ids: NodeKey[] = [];
 
               if (mutation === 'destroyed') {
-                if (markNodeKeys !== undefined) {
-                  markNodeKeys.delete(key);
-                  if (markNodeKeys.size === 0) {
-                    markNodeMap.delete(id);
+                ids = markNodeKeysToIDs.get(key) || [];
+              } else if ($isMarkNode(node)) {
+                ids = node.getIDs();
+              }
+
+              for (let i = 0; i < ids.length; i++) {
+                const id = ids[i];
+                let markNodeKeys = markNodeMap.get(id);
+                markNodeKeysToIDs.set(key, ids);
+
+                if (mutation === 'destroyed') {
+                  if (markNodeKeys !== undefined) {
+                    markNodeKeys.delete(key);
+                    if (markNodeKeys.size === 0) {
+                      markNodeMap.delete(id);
+                    }
+                  }
+                } else {
+                  if (markNodeKeys === undefined) {
+                    markNodeKeys = new Set();
+                    markNodeMap.set(id, markNodeKeys);
+                  }
+                  if (!markNodeKeys.has(key)) {
+                    markNodeKeys.add(key);
                   }
-                }
-              } else {
-                if (markNodeKeys === undefined) {
-                  markNodeKeys = new Set();
-                  markNodeMap.set(id, markNodeKeys);
-                }
-                if (!markNodeKeys.has(key)) {
-                  markNodeKeys.add(key);
                 }
               }
             }
-          }
-        });
-      }),
+          });
+        },
+        {skipInitialization: false},
+      ),
       editor.registerUpdateListener(({editorState, tags}) => {
         editorState.read(() => {
           const selection = $getSelection();
diff --git a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx
index 79a1d150590..eb72fd9d7dc 100644
--- a/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx
+++ b/packages/lexical-playground/src/plugins/TableActionMenuPlugin/index.tsx
@@ -183,17 +183,21 @@ function TableActionMenu({
   );
 
   useEffect(() => {
-    return editor.registerMutationListener(TableCellNode, (nodeMutations) => {
-      const nodeUpdated =
-        nodeMutations.get(tableCellNode.getKey()) === 'updated';
-
-      if (nodeUpdated) {
-        editor.getEditorState().read(() => {
-          updateTableCellNode(tableCellNode.getLatest());
-        });
-        setBackgroundColor(currentCellBackgroundColor(editor) || '');
-      }
-    });
+    return editor.registerMutationListener(
+      TableCellNode,
+      (nodeMutations) => {
+        const nodeUpdated =
+          nodeMutations.get(tableCellNode.getKey()) === 'updated';
+
+        if (nodeUpdated) {
+          editor.getEditorState().read(() => {
+            updateTableCellNode(tableCellNode.getLatest());
+          });
+          setBackgroundColor(currentCellBackgroundColor(editor) || '');
+        }
+      },
+      {skipInitialization: true},
+    );
   }, [editor, tableCellNode]);
 
   useEffect(() => {
diff --git a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx
index f53b43ec785..e7f186bc57c 100644
--- a/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx
+++ b/packages/lexical-playground/src/plugins/TableHoverActionsPlugin/index.tsx
@@ -19,7 +19,7 @@ import {
   TableRowNode,
 } from '@lexical/table';
 import {$findMatchingParent, mergeRegister} from '@lexical/utils';
-import {$getNearestNodeFromDOMNode} from 'lexical';
+import {$getNearestNodeFromDOMNode, NodeKey} from 'lexical';
 import {useEffect, useRef, useState} from 'react';
 import * as React from 'react';
 import {createPortal} from 'react-dom';
@@ -39,7 +39,7 @@ function TableHoverActionsContainer({
   const [shouldListenMouseMove, setShouldListenMouseMove] =
     useState<boolean>(false);
   const [position, setPosition] = useState({});
-  const codeSetRef = useRef<Set<string>>(new Set());
+  const codeSetRef = useRef<Set<NodeKey>>(new Set());
   const tableDOMNodeRef = useRef<HTMLElement | null>(null);
 
   const debouncedOnMouseMove = useDebounce(
@@ -148,26 +148,30 @@ function TableHoverActionsContainer({
 
   useEffect(() => {
     return mergeRegister(
-      editor.registerMutationListener(TableNode, (mutations) => {
-        editor.getEditorState().read(() => {
-          for (const [key, type] of mutations) {
-            switch (type) {
-              case 'created':
-                codeSetRef.current.add(key);
-                setShouldListenMouseMove(codeSetRef.current.size > 0);
-                break;
-
-              case 'destroyed':
-                codeSetRef.current.delete(key);
-                setShouldListenMouseMove(codeSetRef.current.size > 0);
-                break;
-
-              default:
-                break;
+      editor.registerMutationListener(
+        TableNode,
+        (mutations) => {
+          editor.getEditorState().read(() => {
+            for (const [key, type] of mutations) {
+              switch (type) {
+                case 'created':
+                  codeSetRef.current.add(key);
+                  setShouldListenMouseMove(codeSetRef.current.size > 0);
+                  break;
+
+                case 'destroyed':
+                  codeSetRef.current.delete(key);
+                  setShouldListenMouseMove(codeSetRef.current.size > 0);
+                  break;
+
+                default:
+                  break;
+              }
             }
-          }
-        });
-      }),
+          });
+        },
+        {skipInitialization: false},
+      ),
     );
   }, [editor]);
 
diff --git a/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx b/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx
index 2879d1bafff..49ebbb1a270 100644
--- a/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx
+++ b/packages/lexical-react/src/LexicalAutoEmbedPlugin.tsx
@@ -105,24 +105,23 @@ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({
   }, []);
 
   const checkIfLinkNodeIsEmbeddable = useCallback(
-    (key: NodeKey) => {
-      editor.getEditorState().read(async function () {
+    async (key: NodeKey) => {
+      const url = editor.getEditorState().read(function () {
         const linkNode = $getNodeByKey(key);
         if ($isLinkNode(linkNode)) {
-          for (let i = 0; i < embedConfigs.length; i++) {
-            const embedConfig = embedConfigs[i];
-
-            const urlMatch = await Promise.resolve(
-              embedConfig.parseUrl(linkNode.__url),
-            );
-
-            if (urlMatch != null) {
-              setActiveEmbedConfig(embedConfig);
-              setNodeKey(linkNode.getKey());
-            }
-          }
+          return linkNode.getURL();
         }
       });
+      if (url === undefined) {
+        return;
+      }
+      for (const embedConfig of embedConfigs) {
+        const urlMatch = await Promise.resolve(embedConfig.parseUrl(url));
+        if (urlMatch != null) {
+          setActiveEmbedConfig(embedConfig);
+          setNodeKey(key);
+        }
+      }
     },
     [editor, embedConfigs],
   );
@@ -146,7 +145,9 @@ export function LexicalAutoEmbedPlugin<TEmbedConfig extends EmbedConfig>({
     };
     return mergeRegister(
       ...[LinkNode, AutoLinkNode].map((Klass) =>
-        editor.registerMutationListener(Klass, (...args) => listener(...args)),
+        editor.registerMutationListener(Klass, (...args) => listener(...args), {
+          skipInitialization: true,
+        }),
       ),
     );
   }, [checkIfLinkNodeIsEmbeddable, editor, embedConfigs, nodeKey, reset]);
diff --git a/packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx b/packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx
index ceaa1da772f..86f280e0d44 100644
--- a/packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx
+++ b/packages/lexical-react/src/LexicalTableOfContentsPlugin.tsx
@@ -234,6 +234,8 @@ export function TableOfContentsPlugin({children}: Props): JSX.Element {
           setTableOfContents(currentTableOfContents);
         });
       },
+      // Initialization is handled separately
+      {skipInitialization: true},
     );
 
     // Listen to text node mutation updates
@@ -258,6 +260,8 @@ export function TableOfContentsPlugin({children}: Props): JSX.Element {
           }
         });
       },
+      // Initialization is handled separately
+      {skipInitialization: true},
     );
 
     return () => {
diff --git a/packages/lexical-react/src/LexicalTablePlugin.ts b/packages/lexical-react/src/LexicalTablePlugin.ts
index e237e67b339..2ae1dfd94f7 100644
--- a/packages/lexical-react/src/LexicalTablePlugin.ts
+++ b/packages/lexical-react/src/LexicalTablePlugin.ts
@@ -38,7 +38,6 @@ import {
   $createParagraphNode,
   $getNodeByKey,
   $isTextNode,
-  $nodesOfType,
   COMMAND_PRIORITY_EDITOR,
 } from 'lexical';
 import {useEffect} from 'react';
@@ -129,17 +128,6 @@ export function TablePlugin({
       }
     };
 
-    // Plugins might be loaded _after_ initial content is set, hence existing table nodes
-    // won't be initialized from mutation[create] listener. Instead doing it here,
-    editor.getEditorState().read(() => {
-      const tableNodes = $nodesOfType(TableNode);
-      for (const tableNode of tableNodes) {
-        if ($isTableNode(tableNode)) {
-          initializeTableNode(tableNode);
-        }
-      }
-    });
-
     const unregisterMutationListener = editor.registerMutationListener(
       TableNode,
       (nodeMutations) => {
@@ -161,6 +149,7 @@ export function TablePlugin({
           }
         }
       },
+      {skipInitialization: false},
     );
 
     return () => {
diff --git a/packages/lexical-website/docs/concepts/dom-events.md b/packages/lexical-website/docs/concepts/dom-events.md
index ad411d07a17..c45b5d7615b 100644
--- a/packages/lexical-website/docs/concepts/dom-events.md
+++ b/packages/lexical-website/docs/concepts/dom-events.md
@@ -30,20 +30,20 @@ This can be a simple, efficient way to handle some use cases, since it's not nec
 
 ## 2. Directly Attach Handlers
 
-In some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that you're handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](https://lexical.dev/docs/concepts/listeners).
+In some cases, it may be better to attach an event handler directly to the underlying DOM node of each specific node. With this approach, you generally don't need to filter the event target in the handler, which can make it a bit simpler. It will also guarantee that your handler isn't running for events that you don't care about. This approach is implemented via a [Mutation Listener](https://lexical.dev/docs/concepts/listeners).
 
 ```js
+const registeredElements: WeakSet<HTMLElement> = new WeakSet();
 const removeMutationListener = editor.registerMutationListener(nodeType, (mutations) => {
-    const registeredElements: WeakSet<HTMLElement> = new WeakSet();
     editor.getEditorState().read(() => {
         for (const [key, mutation] of mutations) {
             const element: null | HTMLElement = editor.getElementByKey(key);
             if (
-            // Updated might be a move, so that might mean a new DOM element
-            // is created. In this case, we need to add and event listener too.
-            (mutation === 'created' || mutation === 'updated') &&
-            element !== null &&
-            !registeredElements.has(element)
+                // Updated might be a move, so that might mean a new DOM element
+                // is created. In this case, we need to add and event listener too.
+                (mutation === 'created' || mutation === 'updated') &&
+                element !== null &&
+                !registeredElements.has(element)
             ) {
                 registeredElements.add(element);
                 element.addEventListener('click', (event: Event) => {
diff --git a/packages/lexical-website/docs/concepts/listeners.md b/packages/lexical-website/docs/concepts/listeners.md
index ca890fbefbc..fb5036223b0 100644
--- a/packages/lexical-website/docs/concepts/listeners.md
+++ b/packages/lexical-website/docs/concepts/listeners.md
@@ -81,15 +81,21 @@ Get notified when a specific type of Lexical node has been mutated. There are th
 Mutation listeners are great for tracking the lifecycle of specific types of node. They can be used to
 handle external UI state and UI features relating to specific types of node.
 
+If any existing nodes are in the DOM, and skipInitialization is not true, the listener
+will be called immediately with an updateTag of 'registerMutationListener' where all
+nodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option
+(default is currently true for backwards compatibility in 0.16.x but will change to false in 0.17.0).
+
 ```js
 const removeMutationListener = editor.registerMutationListener(
   MyCustomNode,
-  (mutatedNodes) => {
+  (mutatedNodes, { updateTags, dirtyLeaves, prevEditorState }) => {
     // mutatedNodes is a Map where each key is the NodeKey, and the value is the state of mutation.
     for (let [nodeKey, mutation] of mutatedNodes) {
       console.log(nodeKey, mutation)
     }
   },
+  {skipInitialization: false}
 );
 
 // Do not forget to unregister the listener when no longer needed!
diff --git a/packages/lexical/flow/Lexical.js.flow b/packages/lexical/flow/Lexical.js.flow
index 916a2dc7dc6..6744da5ed5d 100644
--- a/packages/lexical/flow/Lexical.js.flow
+++ b/packages/lexical/flow/Lexical.js.flow
@@ -109,6 +109,9 @@ export type MutationListener = (
     prevEditorState: EditorState,
   },
 ) => void;
+export type MutationListenerOptions = {
+  skipInitialization?: boolean;
+};
 export type EditableListener = (editable: boolean) => void;
 type Listeners = {
   decorator: Set<DecoratorListener>,
@@ -178,6 +181,7 @@ declare export class LexicalEditor {
   registerMutationListener(
     klass: Class<LexicalNode>,
     listener: MutationListener,
+    options?: MutationListenerOptions,
   ): () => void;
   registerNodeTransform<T: LexicalNode>(
     klass: Class<T>,
diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts
index 29cd66b7598..ce7a22b5992 100644
--- a/packages/lexical/src/LexicalEditor.ts
+++ b/packages/lexical/src/LexicalEditor.ts
@@ -33,6 +33,7 @@ import {
   createUID,
   dispatchCommand,
   getCachedClassNameArray,
+  getCachedTypeToNodeMap,
   getDefaultView,
   getDOMSelection,
   markAllNodesAsDirty,
@@ -211,6 +212,18 @@ export type MutatedNodes = Map<Klass<LexicalNode>, Map<NodeKey, NodeMutation>>;
 
 export type NodeMutation = 'created' | 'updated' | 'destroyed';
 
+export interface MutationListenerOptions {
+  /**
+   * Skip the initial call of the listener with pre-existing DOM nodes.
+   *
+   * The default is currently true for backwards compatibility with <= 0.16.1
+   * but this default is expected to change to false in 0.17.0.
+   */
+  skipInitialization?: boolean;
+}
+
+const DEFAULT_SKIP_INITIALIZATION = true;
+
 export type UpdateListener = (arg0: {
   dirtyElements: Map<NodeKey, IntentionallyMarkedAsDirtyElement>;
   dirtyLeaves: Set<NodeKey>;
@@ -824,15 +837,43 @@ export class LexicalEditor {
    * One common use case for this is to attach DOM event listeners to the underlying DOM nodes as Lexical nodes are created.
    * {@link LexicalEditor.getElementByKey} can be used for this.
    *
+   * If any existing nodes are in the DOM, and skipInitialization is not true, the listener
+   * will be called immediately with an updateTag of 'registerMutationListener' where all
+   * nodes have the 'created' NodeMutation. This can be controlled with the skipInitialization option
+   * (default is currently true for backwards compatibility in 0.16.x but will change to false in 0.17.0).
+   *
    * @param klass - The class of the node that you want to listen to mutations on.
    * @param listener - The logic you want to run when the node is mutated.
+   * @param options - see {@link MutationListenerOptions}
    * @returns a teardown function that can be used to cleanup the listener.
    */
   registerMutationListener(
     klass: Klass<LexicalNode>,
     listener: MutationListener,
+    options?: MutationListenerOptions,
   ): () => void {
-    let registeredNode = this._nodes.get(klass.getType());
+    const klassToMutate = this.resolveRegisteredNodeAfterReplacements(
+      this.getRegisteredNode(klass),
+    ).klass;
+    const mutations = this._listeners.mutation;
+    mutations.set(listener, klassToMutate);
+    const skipInitialization = options && options.skipInitialization;
+    if (
+      !(skipInitialization === undefined
+        ? DEFAULT_SKIP_INITIALIZATION
+        : skipInitialization)
+    ) {
+      this.initializeMutationListener(listener, klassToMutate);
+    }
+
+    return () => {
+      mutations.delete(listener);
+    };
+  }
+
+  /** @internal */
+  private getRegisteredNode(klass: Klass<LexicalNode>): RegisteredNode {
+    const registeredNode = this._nodes.get(klass.getType());
 
     if (registeredNode === undefined) {
       invariant(
@@ -842,29 +883,42 @@ export class LexicalEditor {
       );
     }
 
-    let klassToMutate = klass;
-
-    let replaceKlass: Klass<LexicalNode> | null = null;
-    while ((replaceKlass = registeredNode.replaceWithKlass)) {
-      klassToMutate = replaceKlass;
-
-      registeredNode = this._nodes.get(replaceKlass.getType());
+    return registeredNode;
+  }
 
-      if (registeredNode === undefined) {
-        invariant(
-          false,
-          'Node %s has not been registered. Ensure node has been passed to createEditor.',
-          replaceKlass.name,
-        );
-      }
+  /** @internal */
+  private resolveRegisteredNodeAfterReplacements(
+    registeredNode: RegisteredNode,
+  ): RegisteredNode {
+    while (registeredNode.replaceWithKlass) {
+      registeredNode = this.getRegisteredNode(registeredNode.replaceWithKlass);
     }
+    return registeredNode;
+  }
 
-    const mutations = this._listeners.mutation;
-    mutations.set(listener, klassToMutate);
-
-    return () => {
-      mutations.delete(listener);
-    };
+  /** @internal */
+  private initializeMutationListener(
+    listener: MutationListener,
+    klass: Klass<LexicalNode>,
+  ): void {
+    const prevEditorState = this._editorState;
+    const nodeMap = getCachedTypeToNodeMap(this._editorState).get(
+      klass.getType(),
+    );
+    if (!nodeMap) {
+      return;
+    }
+    const nodeMutationMap = new Map<string, NodeMutation>();
+    for (const k of nodeMap.keys()) {
+      nodeMutationMap.set(k, 'created');
+    }
+    if (nodeMutationMap.size > 0) {
+      listener(nodeMutationMap, {
+        dirtyLeaves: new Set(),
+        prevEditorState,
+        updateTags: new Set(['registerMutationListener']),
+      });
+    }
   }
 
   /** @internal */
@@ -872,19 +926,8 @@ export class LexicalEditor {
     klass: Klass<T>,
     listener: Transform<T>,
   ): RegisteredNode {
-    const type = klass.getType();
-
-    const registeredNode = this._nodes.get(type);
-
-    if (registeredNode === undefined) {
-      invariant(
-        false,
-        'Node %s has not been registered. Ensure node has been passed to createEditor.',
-        klass.name,
-      );
-    }
-    const transforms = registeredNode.transforms;
-    transforms.add(listener as Transform<LexicalNode>);
+    const registeredNode = this.getRegisteredNode(klass);
+    registeredNode.transforms.add(listener as Transform<LexicalNode>);
 
     return registeredNode;
   }
diff --git a/packages/lexical/src/LexicalUtils.ts b/packages/lexical/src/LexicalUtils.ts
index eda211b64ed..8cc3099462d 100644
--- a/packages/lexical/src/LexicalUtils.ts
+++ b/packages/lexical/src/LexicalUtils.ts
@@ -1118,16 +1118,21 @@ export function setMutatedNode(
 }
 
 export function $nodesOfType<T extends LexicalNode>(klass: Klass<T>): Array<T> {
-  const editorState = getActiveEditorState();
-  const readOnly = editorState._readOnly;
   const klassType = klass.getType();
+  const editorState = getActiveEditorState();
+  if (editorState._readOnly) {
+    const nodes = getCachedTypeToNodeMap(editorState).get(klassType) as
+      | undefined
+      | Map<string, T>;
+    return nodes ? [...nodes.values()] : [];
+  }
   const nodes = editorState._nodeMap;
   const nodesOfType: Array<T> = [];
   for (const [, node] of nodes) {
     if (
       node instanceof klass &&
       node.__type === klassType &&
-      (readOnly || node.isAttached())
+      node.isAttached()
     ) {
       nodesOfType.push(node as T);
     }
@@ -1691,3 +1696,34 @@ export function $getAncestor<NodeType extends LexicalNode = LexicalNode>(
 export function $getEditor(): LexicalEditor {
   return getActiveEditor();
 }
+
+/** @internal */
+export type TypeToNodeMap = Map<string, NodeMap>;
+/**
+ * @internal
+ * Compute a cached Map of node type to nodes for a frozen EditorState
+ */
+const cachedNodeMaps = new WeakMap<EditorState, TypeToNodeMap>();
+export function getCachedTypeToNodeMap(
+  editorState: EditorState,
+): TypeToNodeMap {
+  invariant(
+    editorState._readOnly,
+    'getCachedTypeToNodeMap called with a writable EditorState',
+  );
+  let typeToNodeMap = cachedNodeMaps.get(editorState);
+  if (!typeToNodeMap) {
+    typeToNodeMap = new Map();
+    cachedNodeMaps.set(editorState, typeToNodeMap);
+    for (const [nodeKey, node] of editorState._nodeMap) {
+      const nodeType = node.__type;
+      let nodeMap = typeToNodeMap.get(nodeType);
+      if (!nodeMap) {
+        nodeMap = new Map();
+        typeToNodeMap.set(nodeType, nodeMap);
+      }
+      nodeMap.set(nodeKey, node);
+    }
+  }
+  return typeToNodeMap;
+}
diff --git a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx
index 5cc8fb8054e..1f069be7194 100644
--- a/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx
+++ b/packages/lexical/src/__tests__/unit/LexicalEditor.test.tsx
@@ -1713,8 +1713,12 @@ describe('LexicalEditor tests', () => {
 
     const paragraphNodeMutations = jest.fn();
     const textNodeMutations = jest.fn();
-    editor.registerMutationListener(ParagraphNode, paragraphNodeMutations);
-    editor.registerMutationListener(TextNode, textNodeMutations);
+    editor.registerMutationListener(ParagraphNode, paragraphNodeMutations, {
+      skipInitialization: false,
+    });
+    editor.registerMutationListener(TextNode, textNodeMutations, {
+      skipInitialization: false,
+    });
     const paragraphKeys: string[] = [];
     const textNodeKeys: string[] = [];
 
@@ -1786,7 +1790,9 @@ describe('LexicalEditor tests', () => {
 
     const initialEditorState = editor.getEditorState();
     const textNodeMutations = jest.fn();
-    editor.registerMutationListener(TextNode, textNodeMutations);
+    editor.registerMutationListener(TextNode, textNodeMutations, {
+      skipInitialization: false,
+    });
     const textNodeKeys: string[] = [];
 
     await editor.update(() => {
@@ -1856,7 +1862,10 @@ describe('LexicalEditor tests', () => {
     });
 
     const textNodeMutations = jest.fn();
-    editor.registerMutationListener(TextNode, textNodeMutations);
+    const textNodeMutationsB = jest.fn();
+    editor.registerMutationListener(TextNode, textNodeMutations, {
+      skipInitialization: false,
+    });
     const textNodeKeys: string[] = [];
 
     // No await intentional (batch with next)
@@ -1879,6 +1888,10 @@ describe('LexicalEditor tests', () => {
       textNodeKeys.push(textNode3.getKey());
     });
 
+    editor.registerMutationListener(TextNode, textNodeMutationsB, {
+      skipInitialization: false,
+    });
+
     await editor.update(() => {
       $getRoot().clear();
     });
@@ -1893,6 +1906,7 @@ describe('LexicalEditor tests', () => {
     });
 
     expect(textNodeMutations.mock.calls.length).toBe(2);
+    expect(textNodeMutationsB.mock.calls.length).toBe(2);
 
     const [textNodeMutation1, textNodeMutation2] = textNodeMutations.mock.calls;
 
@@ -1900,10 +1914,28 @@ describe('LexicalEditor tests', () => {
     expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('created');
     expect(textNodeMutation1[0].get(textNodeKeys[1])).toBe('created');
     expect(textNodeMutation1[0].get(textNodeKeys[2])).toBe('created');
+    expect([...textNodeMutation1[1].updateTags]).toEqual([]);
     expect(textNodeMutation2[0].size).toBe(3);
     expect(textNodeMutation2[0].get(textNodeKeys[0])).toBe('destroyed');
     expect(textNodeMutation2[0].get(textNodeKeys[1])).toBe('destroyed');
     expect(textNodeMutation2[0].get(textNodeKeys[2])).toBe('destroyed');
+    expect([...textNodeMutation2[1].updateTags]).toEqual([]);
+
+    const [textNodeMutationB1, textNodeMutationB2] =
+      textNodeMutationsB.mock.calls;
+
+    expect(textNodeMutationB1[0].size).toBe(3);
+    expect(textNodeMutationB1[0].get(textNodeKeys[0])).toBe('created');
+    expect(textNodeMutationB1[0].get(textNodeKeys[1])).toBe('created');
+    expect(textNodeMutationB1[0].get(textNodeKeys[2])).toBe('created');
+    expect([...textNodeMutationB1[1].updateTags]).toEqual([
+      'registerMutationListener',
+    ]);
+    expect(textNodeMutationB2[0].size).toBe(3);
+    expect(textNodeMutationB2[0].get(textNodeKeys[0])).toBe('destroyed');
+    expect(textNodeMutationB2[0].get(textNodeKeys[1])).toBe('destroyed');
+    expect(textNodeMutationB2[0].get(textNodeKeys[2])).toBe('destroyed');
+    expect([...textNodeMutationB2[1].updateTags]).toEqual([]);
   });
 
   it('mutation listener should work with the replaced node', async () => {
@@ -1927,10 +1959,12 @@ describe('LexicalEditor tests', () => {
     });
 
     const textNodeMutations = jest.fn();
-    editor.registerMutationListener(TestTextNode, textNodeMutations);
+    const textNodeMutationsB = jest.fn();
+    editor.registerMutationListener(TestTextNode, textNodeMutations, {
+      skipInitialization: false,
+    });
     const textNodeKeys: string[] = [];
 
-    // No await intentional (batch with next)
     await editor.update(() => {
       const root = $getRoot();
       const paragraph = $createParagraphNode();
@@ -1940,12 +1974,25 @@ describe('LexicalEditor tests', () => {
       textNodeKeys.push(textNode.getKey());
     });
 
+    editor.registerMutationListener(TestTextNode, textNodeMutationsB, {
+      skipInitialization: false,
+    });
+
     expect(textNodeMutations.mock.calls.length).toBe(1);
 
     const [textNodeMutation1] = textNodeMutations.mock.calls;
 
     expect(textNodeMutation1[0].size).toBe(1);
     expect(textNodeMutation1[0].get(textNodeKeys[0])).toBe('created');
+    expect([...textNodeMutation1[1].updateTags]).toEqual([]);
+
+    const [textNodeMutationB1] = textNodeMutationsB.mock.calls;
+
+    expect(textNodeMutationB1[0].size).toBe(1);
+    expect(textNodeMutationB1[0].get(textNodeKeys[0])).toBe('created');
+    expect([...textNodeMutationB1[1].updateTags]).toEqual([
+      'registerMutationListener',
+    ]);
   });
 
   it('mutation listeners does not trigger when other node types are mutated', async () => {
@@ -1953,8 +2000,12 @@ describe('LexicalEditor tests', () => {
 
     const paragraphNodeMutations = jest.fn();
     const textNodeMutations = jest.fn();
-    editor.registerMutationListener(ParagraphNode, paragraphNodeMutations);
-    editor.registerMutationListener(TextNode, textNodeMutations);
+    editor.registerMutationListener(ParagraphNode, paragraphNodeMutations, {
+      skipInitialization: false,
+    });
+    editor.registerMutationListener(TextNode, textNodeMutations, {
+      skipInitialization: false,
+    });
 
     await editor.update(() => {
       $getRoot().append($createParagraphNode());
@@ -1968,7 +2019,9 @@ describe('LexicalEditor tests', () => {
     init();
 
     const textNodeMutations = jest.fn();
-    editor.registerMutationListener(TextNode, textNodeMutations);
+    editor.registerMutationListener(TextNode, textNodeMutations, {
+      skipInitialization: false,
+    });
     const textNodeKeys: string[] = [];
 
     await editor.update(() => {
@@ -2014,8 +2067,12 @@ describe('LexicalEditor tests', () => {
     const paragraphNodeMutations = jest.fn();
     const textNodeMutations = jest.fn();
 
-    editor.registerMutationListener(ParagraphNode, paragraphNodeMutations);
-    editor.registerMutationListener(TextNode, textNodeMutations);
+    editor.registerMutationListener(ParagraphNode, paragraphNodeMutations, {
+      skipInitialization: false,
+    });
+    editor.registerMutationListener(TextNode, textNodeMutations, {
+      skipInitialization: false,
+    });
 
     const paragraphNodeKeys: string[] = [];
     const textNodeKeys: string[] = [];
@@ -2074,8 +2131,12 @@ describe('LexicalEditor tests', () => {
     const tableCellMutations = jest.fn();
     const tableRowMutations = jest.fn();
 
-    editor.registerMutationListener(TableCellNode, tableCellMutations);
-    editor.registerMutationListener(TableRowNode, tableRowMutations);
+    editor.registerMutationListener(TableCellNode, tableCellMutations, {
+      skipInitialization: false,
+    });
+    editor.registerMutationListener(TableRowNode, tableRowMutations, {
+      skipInitialization: false,
+    });
     // Create Table
 
     await editor.update(() => {
@@ -2154,12 +2215,20 @@ describe('LexicalEditor tests', () => {
       });
     });
 
-    editor.registerMutationListener(TextNode, (map) => {
-      mutationListener();
-      editor.registerMutationListener(TextNode, () => {
+    editor.registerMutationListener(
+      TextNode,
+      (map) => {
         mutationListener();
-      });
-    });
+        editor.registerMutationListener(
+          TextNode,
+          () => {
+            mutationListener();
+          },
+          {skipInitialization: true},
+        );
+      },
+      {skipInitialization: false},
+    );
 
     editor.registerNodeTransform(ParagraphNode, () => {
       nodeTransformListener();
@@ -2214,6 +2283,74 @@ describe('LexicalEditor tests', () => {
     expect(mutationListener).toHaveBeenCalledTimes(1);
   });
 
+  it('calls mutation listener with initial state', async () => {
+    // TODO add tests for node replacement
+    const mutationListenerA = jest.fn();
+    const mutationListenerB = jest.fn();
+    const mutationListenerC = jest.fn();
+    init();
+
+    editor.registerMutationListener(TextNode, mutationListenerA, {
+      skipInitialization: false,
+    });
+    expect(mutationListenerA).toHaveBeenCalledTimes(0);
+
+    await update(() => {
+      $getRoot().append(
+        $createParagraphNode().append($createTextNode('Hello world')),
+      );
+    });
+
+    function asymmetricMatcher<T>(asymmetricMatch: (x: T) => boolean) {
+      return {asymmetricMatch};
+    }
+
+    expect(mutationListenerA).toHaveBeenCalledTimes(1);
+    expect(mutationListenerA).toHaveBeenLastCalledWith(
+      expect.anything(),
+      expect.objectContaining({
+        updateTags: asymmetricMatcher(
+          (s: Set<string>) => !s.has('registerMutationListener'),
+        ),
+      }),
+    );
+    editor.registerMutationListener(TextNode, mutationListenerB, {
+      skipInitialization: false,
+    });
+    editor.registerMutationListener(TextNode, mutationListenerC, {
+      skipInitialization: true,
+    });
+    expect(mutationListenerA).toHaveBeenCalledTimes(1);
+    expect(mutationListenerB).toHaveBeenCalledTimes(1);
+    expect(mutationListenerB).toHaveBeenLastCalledWith(
+      expect.anything(),
+      expect.objectContaining({
+        updateTags: asymmetricMatcher((s: Set<string>) =>
+          s.has('registerMutationListener'),
+        ),
+      }),
+    );
+    expect(mutationListenerC).toHaveBeenCalledTimes(0);
+    await update(() => {
+      $getRoot().append(
+        $createParagraphNode().append($createTextNode('Another update!')),
+      );
+    });
+    expect(mutationListenerA).toHaveBeenCalledTimes(2);
+    expect(mutationListenerB).toHaveBeenCalledTimes(2);
+    expect(mutationListenerC).toHaveBeenCalledTimes(1);
+    [mutationListenerA, mutationListenerB, mutationListenerC].forEach((fn) => {
+      expect(fn).toHaveBeenLastCalledWith(
+        expect.anything(),
+        expect.objectContaining({
+          updateTags: asymmetricMatcher(
+            (s: Set<string>) => !s.has('registerMutationListener'),
+          ),
+        }),
+      );
+    });
+  });
+
   it('can use flushSync for synchronous updates', () => {
     init();
     const onUpdate = jest.fn();
diff --git a/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts b/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts
index b495d174322..0026cf5d6ad 100644
--- a/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts
+++ b/packages/lexical/src/__tests__/unit/LexicalUtils.test.ts
@@ -13,6 +13,7 @@ import {
   $nodesOfType,
   emptyFunction,
   generateRandomKey,
+  getCachedTypeToNodeMap,
   getTextDirection,
   isArray,
   isSelectionWithinEditor,
@@ -235,5 +236,58 @@ describe('LexicalUtils tests', () => {
         );
       });
     });
+
+    test('getCachedTypeToNodeMap', async () => {
+      const {editor} = testEnv;
+      const paragraphKeys: string[] = [];
+
+      const initialTypeToNodeMap = getCachedTypeToNodeMap(
+        editor.getEditorState(),
+      );
+      expect(getCachedTypeToNodeMap(editor.getEditorState())).toBe(
+        initialTypeToNodeMap,
+      );
+      expect([...initialTypeToNodeMap.keys()]).toEqual(['root']);
+      expect(initialTypeToNodeMap.get('root')).toMatchObject({size: 1});
+
+      editor.update(
+        () => {
+          const root = $getRoot();
+          const paragraph1 = $createParagraphNode().append(
+            $createTextNode('a'),
+          );
+          const paragraph2 = $createParagraphNode().append(
+            $createTextNode('b'),
+          );
+          // these will be garbage collected and not in the readonly map
+          $createParagraphNode().append($createTextNode('c'));
+          root.append(paragraph1, paragraph2);
+          paragraphKeys.push(paragraph1.getKey(), paragraph2.getKey());
+        },
+        {discrete: true},
+      );
+
+      const typeToNodeMap = getCachedTypeToNodeMap(editor.getEditorState());
+      // verify that the initial cache was not used
+      expect(typeToNodeMap).not.toBe(initialTypeToNodeMap);
+      // verify that the cache is used for subsequent calls
+      expect(getCachedTypeToNodeMap(editor.getEditorState())).toBe(
+        typeToNodeMap,
+      );
+      expect(typeToNodeMap.size).toEqual(3);
+      expect([...typeToNodeMap.keys()]).toEqual(
+        expect.arrayContaining(['root', 'paragraph', 'text']),
+      );
+      const paragraphMap = typeToNodeMap.get('paragraph')!;
+      expect(paragraphMap.size).toEqual(paragraphKeys.length);
+      expect([...paragraphMap.keys()]).toEqual(
+        expect.arrayContaining(paragraphKeys),
+      );
+      const textMap = typeToNodeMap.get('text')!;
+      expect(textMap.size).toEqual(2);
+      expect(
+        [...textMap.values()].map((node) => (node as TextNode).__text),
+      ).toEqual(expect.arrayContaining(['a', 'b']));
+    });
   });
 });