diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js
index 620dc314485e1..e736f2ffb53a8 100644
--- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js
+++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js
@@ -55,7 +55,12 @@ import {
   setValueForStyles,
   validateShorthandPropertyCollisionInDev,
 } from './CSSPropertyOperations';
-import {HTML_NAMESPACE, getIntrinsicNamespace} from '../shared/DOMNamespaces';
+import {
+  HTML_NAMESPACE,
+  MATH_NAMESPACE,
+  SVG_NAMESPACE,
+  getIntrinsicNamespace,
+} from '../shared/DOMNamespaces';
 import {
   getPropertyInfo,
   shouldIgnoreAttribute,
@@ -375,112 +380,112 @@ function updateDOMProperties(
   }
 }
 
-export function createElement(
+// Creates elements in the HTML namesapce
+export function createHTMLElement(
   type: string,
   props: Object,
-  rootContainerElement: Element | Document | DocumentFragment,
-  parentNamespace: string,
+  ownerDocument: Document,
 ): Element {
   let isCustomComponentTag;
 
-  // We create tags in the namespace of their parent container, except HTML
-  // tags get no namespace.
-  const ownerDocument: Document =
-    getOwnerDocumentFromRootContainer(rootContainerElement);
   let domElement: Element;
-  let namespaceURI = parentNamespace;
-  if (namespaceURI === HTML_NAMESPACE) {
-    namespaceURI = getIntrinsicNamespace(type);
+  if (__DEV__) {
+    isCustomComponentTag = isCustomComponent(type, props);
+    // Should this check be gated by parent namespace? Not sure we want to
+    // allow <SVG> or <mATH>.
+    if (!isCustomComponentTag && type !== type.toLowerCase()) {
+      console.error(
+        '<%s /> is using incorrect casing. ' +
+          'Use PascalCase for React components, ' +
+          'or lowercase for HTML elements.',
+        type,
+      );
+    }
   }
-  if (namespaceURI === HTML_NAMESPACE) {
+
+  if (type === 'script') {
+    // Create the script via .innerHTML so its "parser-inserted" flag is
+    // set to true and it does not execute
+    const div = ownerDocument.createElement('div');
     if (__DEV__) {
-      isCustomComponentTag = isCustomComponent(type, props);
-      // Should this check be gated by parent namespace? Not sure we want to
-      // allow <SVG> or <mATH>.
-      if (!isCustomComponentTag && type !== type.toLowerCase()) {
+      if (enableTrustedTypesIntegration && !didWarnScriptTags) {
         console.error(
-          '<%s /> is using incorrect casing. ' +
-            'Use PascalCase for React components, ' +
-            'or lowercase for HTML elements.',
-          type,
+          'Encountered a script tag while rendering React component. ' +
+            'Scripts inside React components are never executed when rendering ' +
+            'on the client. Consider using template tag instead ' +
+            '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
         );
+        didWarnScriptTags = true;
       }
     }
-
-    if (type === 'script') {
-      // Create the script via .innerHTML so its "parser-inserted" flag is
-      // set to true and it does not execute
-      const div = ownerDocument.createElement('div');
-      if (__DEV__) {
-        if (enableTrustedTypesIntegration && !didWarnScriptTags) {
-          console.error(
-            'Encountered a script tag while rendering React component. ' +
-              'Scripts inside React components are never executed when rendering ' +
-              'on the client. Consider using template tag instead ' +
-              '(https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template).',
-          );
-          didWarnScriptTags = true;
-        }
-      }
-      div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
-      // This is guaranteed to yield a script element.
-      const firstChild = ((div.firstChild: any): HTMLScriptElement);
-      domElement = div.removeChild(firstChild);
-    } else if (typeof props.is === 'string') {
-      domElement = ownerDocument.createElement(type, {is: props.is});
-    } else {
-      // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
-      // See discussion in https://github.com/facebook/react/pull/6896
-      // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
-      domElement = ownerDocument.createElement(type);
-      // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
-      // attributes on `select`s needs to be added before `option`s are inserted.
-      // This prevents:
-      // - a bug where the `select` does not scroll to the correct option because singular
-      //  `select` elements automatically pick the first item #13222
-      // - a bug where the `select` set the first item as selected despite the `size` attribute #14239
-      // See https://github.com/facebook/react/issues/13222
-      // and https://github.com/facebook/react/issues/14239
-      if (type === 'select') {
-        const node = ((domElement: any): HTMLSelectElement);
-        if (props.multiple) {
-          node.multiple = true;
-        } else if (props.size) {
-          // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
-          // it is possible that no option is selected.
-          //
-          // This is only necessary when a select in "single selection mode".
-          node.size = props.size;
-        }
+    div.innerHTML = '<script><' + '/script>'; // eslint-disable-line
+    // This is guaranteed to yield a script element.
+    const firstChild = ((div.firstChild: any): HTMLScriptElement);
+    domElement = div.removeChild(firstChild);
+  } else if (typeof props.is === 'string') {
+    domElement = ownerDocument.createElement(type, {is: props.is});
+  } else {
+    // Separate else branch instead of using `props.is || undefined` above because of a Firefox bug.
+    // See discussion in https://github.com/facebook/react/pull/6896
+    // and discussion in https://bugzilla.mozilla.org/show_bug.cgi?id=1276240
+    domElement = ownerDocument.createElement(type);
+    // Normally attributes are assigned in `setInitialDOMProperties`, however the `multiple` and `size`
+    // attributes on `select`s needs to be added before `option`s are inserted.
+    // This prevents:
+    // - a bug where the `select` does not scroll to the correct option because singular
+    //  `select` elements automatically pick the first item #13222
+    // - a bug where the `select` set the first item as selected despite the `size` attribute #14239
+    // See https://github.com/facebook/react/issues/13222
+    // and https://github.com/facebook/react/issues/14239
+    if (type === 'select') {
+      const node = ((domElement: any): HTMLSelectElement);
+      if (props.multiple) {
+        node.multiple = true;
+      } else if (props.size) {
+        // Setting a size greater than 1 causes a select to behave like `multiple=true`, where
+        // it is possible that no option is selected.
+        //
+        // This is only necessary when a select in "single selection mode".
+        node.size = props.size;
       }
     }
-  } else {
-    domElement = ownerDocument.createElementNS(namespaceURI, type);
   }
 
   if (__DEV__) {
-    if (namespaceURI === HTML_NAMESPACE) {
-      if (
-        !isCustomComponentTag &&
-        // $FlowFixMe[method-unbinding]
-        Object.prototype.toString.call(domElement) ===
-          '[object HTMLUnknownElement]' &&
-        !hasOwnProperty.call(warnedUnknownTags, type)
-      ) {
-        warnedUnknownTags[type] = true;
-        console.error(
-          'The tag <%s> is unrecognized in this browser. ' +
-            'If you meant to render a React component, start its name with ' +
-            'an uppercase letter.',
-          type,
-        );
-      }
+    if (
+      !isCustomComponentTag &&
+      // $FlowFixMe[method-unbinding]
+      Object.prototype.toString.call(domElement) ===
+        '[object HTMLUnknownElement]' &&
+      !hasOwnProperty.call(warnedUnknownTags, type)
+    ) {
+      warnedUnknownTags[type] = true;
+      console.error(
+        'The tag <%s> is unrecognized in this browser. ' +
+          'If you meant to render a React component, start its name with ' +
+          'an uppercase letter.',
+        type,
+      );
     }
   }
 
   return domElement;
 }
 
+export function createSVGElement(
+  type: string,
+  ownerDocument: Document,
+): Element {
+  return ownerDocument.createElementNS(SVG_NAMESPACE, type);
+}
+
+export function createMathElement(
+  type: string,
+  ownerDocument: Document,
+): Element {
+  return ownerDocument.createElementNS(MATH_NAMESPACE, type);
+}
+
 export function createTextNode(
   text: string,
   rootContainerElement: Element | Document | DocumentFragment,
@@ -864,9 +869,9 @@ export function diffHydratedProperties(
   domElement: Element,
   tag: string,
   rawProps: Object,
-  parentNamespace: string,
   isConcurrentMode: boolean,
   shouldWarnDev: boolean,
+  parentNamespaceDev: string,
 ): null | Array<mixed> {
   let isCustomComponentTag;
   let extraAttributeNames: Set<string>;
@@ -1109,11 +1114,11 @@ export function diffHydratedProperties(
             propertyInfo,
           );
         } else {
-          let ownNamespace = parentNamespace;
-          if (ownNamespace === HTML_NAMESPACE) {
-            ownNamespace = getIntrinsicNamespace(tag);
+          let ownNamespaceDev = parentNamespaceDev;
+          if (ownNamespaceDev === HTML_NAMESPACE) {
+            ownNamespaceDev = getIntrinsicNamespace(tag);
           }
-          if (ownNamespace === HTML_NAMESPACE) {
+          if (ownNamespaceDev === HTML_NAMESPACE) {
             // $FlowFixMe - Should be inferred as not undefined.
             extraAttributeNames.delete(propKey.toLowerCase());
           } else {
diff --git a/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js b/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
index 89e7f6bc0ac29..8a6eac6987667 100644
--- a/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
+++ b/packages/react-dom-bindings/src/client/ReactDOMFloatClient.js
@@ -9,30 +9,32 @@
 
 import type {Instance, Container} from './ReactDOMHostConfig';
 
-import {isAttributeNameSafe} from '../shared/DOMProperty';
-import {precacheFiberNode} from './ReactDOMComponentTree';
+import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
 
 import ReactDOMSharedInternals from 'shared/ReactDOMSharedInternals.js';
 const {Dispatcher} = ReactDOMSharedInternals;
+import {
+  checkAttributeStringCoercion,
+  checkPropStringCoercion,
+} from 'shared/CheckStringCoercion';
+
 import {DOCUMENT_NODE} from '../shared/HTMLNodeType';
+import {isAttributeNameSafe} from '../shared/DOMProperty';
+import {SVG_NAMESPACE} from '../shared/DOMNamespaces';
 import {
   validatePreloadArguments,
   validatePreinitArguments,
   getValueDescriptorExpectingObjectForWarning,
   getValueDescriptorExpectingEnumForWarning,
 } from '../shared/ReactDOMResourceValidation';
-import {createElement, setInitialProperties} from './ReactDOMComponent';
-import {
-  checkAttributeStringCoercion,
-  checkPropStringCoercion,
-} from 'shared/CheckStringCoercion';
+
+import {precacheFiberNode} from './ReactDOMComponentTree';
+import {createHTMLElement, setInitialProperties} from './ReactDOMComponent';
 import {
   getResourcesFromRoot,
   isMarkedResource,
   markNodeAsResource,
 } from './ReactDOMComponentTree';
-import {HTML_NAMESPACE, SVG_NAMESPACE} from '../shared/DOMNamespaces';
-import {getCurrentRootHostContainer} from 'react-reconciler/src/ReactFiberHostContext';
 
 // The resource types we support. currently they match the form for the as argument.
 // In the future this may need to change, especially when modules / scripts are supported
@@ -174,11 +176,10 @@ function preconnectAs(
 
       const preconnectProps = {rel, crossOrigin, href};
       if (null === ownerDocument.querySelector(key)) {
-        const preloadInstance = createElement(
+        const preloadInstance = createHTMLElement(
           'link',
           preconnectProps,
           ownerDocument,
-          HTML_NAMESPACE,
         );
         setInitialProperties(preloadInstance, 'link', preconnectProps);
         markNodeAsResource(preloadInstance);
@@ -289,11 +290,10 @@ function preload(href: string, options: PreloadOptions) {
       preloadPropsMap.set(key, preloadProps);
 
       if (null === ownerDocument.querySelector(preloadKey)) {
-        const preloadInstance = createElement(
+        const preloadInstance = createHTMLElement(
           'link',
           preloadProps,
           ownerDocument,
-          HTML_NAMESPACE,
         );
         setInitialProperties(preloadInstance, 'link', preloadProps);
         markNodeAsResource(preloadInstance);
@@ -371,11 +371,10 @@ function preinit(href: string, options: PreinitOptions) {
             preloadPropsMap.set(key, preloadProps);
 
             if (null === preloadDocument.querySelector(preloadKey)) {
-              const preloadInstance = createElement(
+              const preloadInstance = createHTMLElement(
                 'link',
                 preloadProps,
                 preloadDocument,
-                HTML_NAMESPACE,
               );
               setInitialProperties(preloadInstance, 'link', preloadProps);
               markNodeAsResource(preloadInstance);
@@ -417,12 +416,8 @@ function preinit(href: string, options: PreinitOptions) {
           if (preloadProps) {
             adoptPreloadPropsForStylesheet(stylesheetProps, preloadProps);
           }
-          instance = createElement(
-            'link',
-            stylesheetProps,
-            resourceRoot,
-            HTML_NAMESPACE,
-          );
+          const ownerDocument = getDocumentFromRoot(resourceRoot);
+          instance = createHTMLElement('link', stylesheetProps, ownerDocument);
           markNodeAsResource(instance);
           setInitialProperties(instance, 'link', stylesheetProps);
           insertStylesheet(instance, precedence, resourceRoot);
@@ -463,12 +458,8 @@ function preinit(href: string, options: PreinitOptions) {
           if (preloadProps) {
             adoptPreloadPropsForScript(scriptProps, preloadProps);
           }
-          instance = createElement(
-            'script',
-            scriptProps,
-            resourceRoot,
-            HTML_NAMESPACE,
-          );
+          const ownerDocument = getDocumentFromRoot(resourceRoot);
+          instance = createHTMLElement('script', scriptProps, ownerDocument);
           markNodeAsResource(instance);
           setInitialProperties(instance, 'link', scriptProps);
           (getDocumentFromRoot(resourceRoot).head: any).appendChild(instance);
@@ -703,11 +694,10 @@ function preloadStylesheet(
       null ===
       ownerDocument.querySelector(getPreloadStylesheetSelectorFromKey(key))
     ) {
-      const preloadInstance = createElement(
+      const preloadInstance = createHTMLElement(
         'link',
         preloadProps,
         ownerDocument,
-        HTML_NAMESPACE,
       );
       setInitialProperties(preloadInstance, 'link', preloadProps);
       markNodeAsResource(preloadInstance);
@@ -762,16 +752,13 @@ export function acquireResource(
         );
         if (instance) {
           resource.instance = instance;
+          markNodeAsResource(instance);
           return instance;
         }
 
         const styleProps = styleTagPropsFromRawProps(props);
-        instance = createElement(
-          'style',
-          styleProps,
-          hoistableRoot,
-          HTML_NAMESPACE,
-        );
+        const ownerDocument = getDocumentFromRoot(hoistableRoot);
+        instance = createHTMLElement('style', styleProps, ownerDocument);
 
         markNodeAsResource(instance);
         setInitialProperties(instance, 'style', styleProps);
@@ -793,6 +780,7 @@ export function acquireResource(
         );
         if (instance) {
           resource.instance = instance;
+          markNodeAsResource(instance);
           return instance;
         }
 
@@ -803,12 +791,8 @@ export function acquireResource(
         }
 
         // Construct and insert a new instance
-        instance = createElement(
-          'link',
-          stylesheetProps,
-          hoistableRoot,
-          HTML_NAMESPACE,
-        );
+        const ownerDocument = getDocumentFromRoot(hoistableRoot);
+        instance = createHTMLElement('link', stylesheetProps, ownerDocument);
         markNodeAsResource(instance);
         const linkInstance: HTMLLinkElement = (instance: any);
         (linkInstance: any)._p = new Promise((resolve, reject) => {
@@ -837,6 +821,7 @@ export function acquireResource(
         );
         if (instance) {
           resource.instance = instance;
+          markNodeAsResource(instance);
           return instance;
         }
 
@@ -848,12 +833,8 @@ export function acquireResource(
         }
 
         // Construct and insert a new instance
-        instance = createElement(
-          'script',
-          scriptProps,
-          hoistableRoot,
-          HTML_NAMESPACE,
-        );
+        const ownerDocument = getDocumentFromRoot(hoistableRoot);
+        instance = createHTMLElement('script', scriptProps, ownerDocument);
         markNodeAsResource(instance);
         setInitialProperties(instance, 'link', scriptProps);
         (getDocumentFromRoot(hoistableRoot).head: any).appendChild(instance);
@@ -1092,7 +1073,7 @@ export function hydrateHoistable(
   }
 
   // There is no matching instance to hydrate, we create it now
-  const instance = createElement(type, props, ownerDocument, HTML_NAMESPACE);
+  const instance = createHTMLElement(type, props, ownerDocument);
   setInitialProperties(instance, type, props);
   precacheFiberNode(internalInstanceHandle, instance);
   markNodeAsResource(instance);
diff --git a/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js b/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
index b774d570f5dab..89182928857ef 100644
--- a/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
+++ b/packages/react-dom-bindings/src/client/ReactDOMHostConfig.js
@@ -32,7 +32,9 @@ import {
 export {detachDeletedInstance};
 import {hasRole} from './DOMAccessibilityRoles';
 import {
-  createElement,
+  createHTMLElement,
+  createSVGElement,
+  createMathElement,
   createTextNode,
   setInitialProperties,
   diffProperties,
@@ -58,6 +60,7 @@ import {
 import {
   getChildNamespace,
   SVG_NAMESPACE,
+  MATH_NAMESPACE,
   HTML_NAMESPACE,
 } from '../shared/DOMNamespaces';
 import {
@@ -144,7 +147,7 @@ export interface SuspenseInstance extends Comment {
 export type HydratableInstance = Instance | TextInstance | SuspenseInstance;
 export type PublicInstance = Element | Text;
 type HostContextDev = {
-  namespace: string,
+  namespace: HostContextProd,
   ancestorInfo: AncestorInfoDev,
 };
 type HostContextProd = string;
@@ -178,7 +181,7 @@ export function getRootHostContext(
   rootContainerInstance: Container,
 ): HostContext {
   let type;
-  let namespace;
+  let namespace: HostContextProd;
   const nodeType = rootContainerInstance.nodeType;
   switch (nodeType) {
     case DOCUMENT_NODE:
@@ -274,12 +277,10 @@ export function createHoistableInstance(
   rootContainerInstance: Container,
   internalInstanceHandle: Object,
 ): Instance {
-  const domElement: Instance = createElement(
-    type,
-    props,
+  const ownerDocument = getOwnerDocumentFromRootContainer(
     rootContainerInstance,
-    HTML_NAMESPACE,
   );
+  const domElement: Instance = createHTMLElement(type, props, ownerDocument);
   precacheFiberNode(internalInstanceHandle, domElement);
   updateFiberProps(domElement, props);
   setInitialProperties(domElement, type, props);
@@ -294,10 +295,10 @@ export function createInstance(
   hostContext: HostContext,
   internalInstanceHandle: Object,
 ): Instance {
-  let parentNamespace: string;
+  let namespace;
   if (__DEV__) {
     // TODO: take namespace into account when validating.
-    const hostContextDev = ((hostContext: any): HostContextDev);
+    const hostContextDev: HostContextDev = (hostContext: any);
     validateDOMNesting(type, null, hostContextDev.ancestorInfo);
     if (
       typeof props.children === 'string' ||
@@ -310,16 +311,37 @@ export function createInstance(
       );
       validateDOMNesting(null, string, ownAncestorInfo);
     }
-    parentNamespace = hostContextDev.namespace;
+    namespace = hostContextDev.namespace;
   } else {
-    parentNamespace = ((hostContext: any): HostContextProd);
+    const hostContextProd: HostContextProd = (hostContext: any);
+    namespace = hostContextProd;
   }
-  const domElement: Instance = createElement(
-    type,
-    props,
+
+  const ownerDocument = getOwnerDocumentFromRootContainer(
     rootContainerInstance,
-    parentNamespace,
   );
+
+  let domElement: Instance;
+  create: switch (namespace) {
+    case SVG_NAMESPACE:
+      domElement = createSVGElement(type, ownerDocument);
+      break;
+    case MATH_NAMESPACE:
+      domElement = createMathElement(type, ownerDocument);
+      break;
+    case HTML_NAMESPACE:
+      switch (type) {
+        case 'svg':
+          domElement = createSVGElement(type, ownerDocument);
+          break create;
+        case 'math':
+          domElement = createMathElement(type, ownerDocument);
+          break create;
+      }
+    // eslint-disable-next-line no-fallthrough
+    default:
+      domElement = createHTMLElement(type, props, ownerDocument);
+  }
   precacheFiberNode(internalInstanceHandle, domElement);
   updateFiberProps(domElement, props);
   return domElement;
@@ -825,17 +847,9 @@ export const supportsHydration = true;
 
 // With Resources, some HostComponent types will never be server rendered and need to be
 // inserted without breaking hydration
-export function isHydratable(type: string, props: Props): boolean {
+export function isHydratableType(type: string, props: Props): boolean {
   if (enableFloat) {
-    if (type === 'link') {
-      if (
-        (props: any).rel === 'stylesheet' &&
-        typeof (props: any).precedence !== 'string'
-      ) {
-        return true;
-      }
-      return false;
-    } else if (type === 'script') {
+    if (type === 'script') {
       const {async, onLoad, onError} = (props: any);
       return !(async && (onLoad || onError));
     }
@@ -844,6 +858,129 @@ export function isHydratable(type: string, props: Props): boolean {
     return true;
   }
 }
+export function isHydratableText(text: string): boolean {
+  return text !== '';
+}
+
+export function shouldSkipHydratableForInstance(
+  instance: HydratableInstance,
+  type: string,
+  props: Props,
+): boolean {
+  if (instance.nodeType !== ELEMENT_NODE) {
+    // This is a suspense boundary or Text node.
+    // Suspense Boundaries are never expected to be injected by 3rd parties. If we see one it should be matched
+    // and this is a hydration error.
+    // Text Nodes are also not expected to be injected by 3rd parties. This is less of a guarantee for <body>
+    // but it seems reasonable and conservative to reject this as a hydration error as well
+    return false;
+  } else if (
+    instance.nodeName.toLowerCase() !== type.toLowerCase() ||
+    isMarkedResource(instance)
+  ) {
+    // We are either about to
+    return true;
+  } else {
+    // We have an Element with the right type.
+    const element: Element = (instance: any);
+    const anyProps = (props: any);
+
+    // We are going to try to exclude it if we can definitely identify it as a hoisted Node or if
+    // we can guess that the node is likely hoisted or was inserted by a 3rd party script or browser extension
+    // using high entropy attributes for certain types. This technique will fail for strange insertions like
+    // extension prepending <div> in the <body> but that already breaks before and that is an edge case.
+    switch (type) {
+      // case 'title':
+      //We assume all titles are matchable. You should only have one in the Document, at least in a hoistable scope
+      // and if you are a HostComponent with type title we must either be in an <svg> context or this title must have an `itemProp` prop.
+      case 'meta': {
+        // The only way to opt out of hoisting meta tags is to give it an itemprop attribute. We assume there will be
+        // not 3rd party meta tags that are prepended, accepting the cases where this isn't true because meta tags
+        // are usually only functional for SSR so even in a rare case where we did bind to an injected tag the runtime
+        // implications are minimal
+        if (!element.hasAttribute('itemprop')) {
+          // This is a Hoistable
+          return true;
+        }
+        break;
+      }
+      case 'link': {
+        // Links come in many forms and we do expect 3rd parties to inject them into <head> / <body>. We exclude known resources
+        // and then use high-entroy attributes like href which are almost always used and almost always unique to filter out unlikely
+        // matches.
+        const rel = element.getAttribute('rel');
+        if (rel === 'stylesheet' && element.hasAttribute('data-precedence')) {
+          // This is a stylesheet resource
+          return true;
+        } else if (
+          rel !== anyProps.rel ||
+          element.getAttribute('href') !==
+            (anyProps.href == null ? null : anyProps.href) ||
+          element.getAttribute('crossorigin') !==
+            (anyProps.crossOrigin == null ? null : anyProps.crossOrigin) ||
+          element.getAttribute('title') !==
+            (anyProps.title == null ? null : anyProps.title)
+        ) {
+          // rel + href should usually be enough to uniquely identify a link however crossOrigin can vary for rel preconnect
+          // and title could vary for rel alternate
+          return true;
+        }
+        break;
+      }
+      case 'style': {
+        // Styles are hard to match correctly. We can exclude known resources but otherwise we accept the fact that a non-hoisted style tags
+        // in <head> or <body> are likely never going to be unmounted given their position in the document and the fact they likely hold global styles
+        if (element.hasAttribute('data-precedence')) {
+          // This is a style resource
+          return true;
+        }
+        break;
+      }
+      case 'script': {
+        // Scripts are a little tricky, we exclude known resources and then similar to links try to use high-entropy attributes
+        // to reject poor matches. One challenge with scripts are inline scripts. We don't attempt to check text content which could
+        // in theory lead to a hydration error later if a 3rd party injected an inline script before the React rendered nodes.
+        // Falling back to client rendering if this happens should be seemless though so we will try this hueristic and revisit later
+        // if we learn it is problematic
+        const srcAttr = element.getAttribute('src');
+        if (
+          srcAttr &&
+          element.hasAttribute('async') &&
+          !element.hasAttribute('itemprop')
+        ) {
+          // This is an async script resource
+          return true;
+        } else if (
+          srcAttr !== (anyProps.src == null ? null : anyProps.src) ||
+          element.getAttribute('type') !==
+            (anyProps.type == null ? null : anyProps.type) ||
+          element.getAttribute('crossorigin') !==
+            (anyProps.crossOrigin == null ? null : anyProps.crossOrigin)
+        ) {
+          // This script is for a different src
+          return true;
+        }
+        break;
+      }
+    }
+    // We have excluded the most likely cases of mismatch between hoistable tags, 3rd party script inserted tags,
+    // and browser extension inserted tags. While it is possible this is not the right match it is a decent hueristic
+    // that should work in the vast majority of cases.
+    return false;
+  }
+}
+
+export function shouldSkipHydratableForTextInstance(
+  instance: HydratableInstance,
+): boolean {
+  return instance.nodeType === ELEMENT_NODE;
+}
+
+export function shouldSkipHydratableForSuspenseInstance(
+  instance: HydratableInstance,
+): boolean {
+  return instance.nodeType === ELEMENT_NODE;
+}
 
 export function canHydrateInstance(
   instance: HydratableInstance,
@@ -852,19 +989,21 @@ export function canHydrateInstance(
 ): null | Instance {
   if (
     instance.nodeType !== ELEMENT_NODE ||
-    type.toLowerCase() !== instance.nodeName.toLowerCase()
+    instance.nodeName.toLowerCase() !== type.toLowerCase()
   ) {
     return null;
+  } else {
+    return ((instance: any): Instance);
   }
-  // This has now been refined to an element node.
-  return ((instance: any): Instance);
 }
 
 export function canHydrateTextInstance(
   instance: HydratableInstance,
   text: string,
 ): null | TextInstance {
-  if (text === '' || instance.nodeType !== TEXT_NODE) {
+  if (text === '') return null;
+
+  if (instance.nodeType !== TEXT_NODE) {
     // Empty strings are not parsed by HTML so there won't be a correct match here.
     return null;
   }
@@ -876,7 +1015,6 @@ export function canHydrateSuspenseInstance(
   instance: HydratableInstance,
 ): null | SuspenseInstance {
   if (instance.nodeType !== COMMENT_NODE) {
-    // Empty strings are not parsed by HTML so there won't be a correct match here.
     return null;
   }
   // This has now been refined to a suspense node.
@@ -931,114 +1069,8 @@ function getNextHydratable(node: ?Node) {
   // Skip non-hydratable nodes.
   for (; node != null; node = ((node: any): Node).nextSibling) {
     const nodeType = node.nodeType;
-    if (enableFloat && enableHostSingletons) {
-      if (nodeType === ELEMENT_NODE) {
-        const element: Element = (node: any);
-        switch (element.tagName) {
-          // This is subtle. in SVG scope the title tag is case sensitive. we don't want to skip
-          // titles in svg but we do want to skip them outside of svg. there is an edge case where
-          // you could do `React.createElement('TITLE', ...)` inside an svg scope but the SSR serializer
-          // will still emit lowercase. Practically speaking the only time the DOM will have a non-uppercased
-          // title tagName is if it is inside an svg.
-          // Other Resource types like META, BASE, LINK, and SCRIPT should be treated as resources even inside
-          // svg scope because they are invalid otherwise. We still don't need to handle the lowercase variant
-          // because if they are present in the DOM already they would have been hoisted outside the SVG scope
-          // as Resources. So while it would be correct to skip a <link> inside <svg> and this algorithm won't
-          // skip that link because the tagName will not be uppercased it functionally is irrelevant. If one
-          // tries to render incompatible types such as a non-resource stylesheet inside an svg the server will
-          // emit that invalid html and hydration will fail. In Dev this will present warnings guiding the
-          // developer on how to fix.
-          case 'TITLE':
-          case 'META':
-          case 'HTML':
-          case 'HEAD':
-          case 'BODY': {
-            continue;
-          }
-          case 'LINK': {
-            const linkEl: HTMLLinkElement = (element: any);
-            // All links that are server rendered are resources except
-            // stylesheets that do not have a precedence
-            if (
-              linkEl.rel === 'stylesheet' &&
-              !linkEl.hasAttribute('data-precedence')
-            ) {
-              break;
-            }
-            continue;
-          }
-          case 'STYLE': {
-            const styleEl: HTMLStyleElement = (element: any);
-            if (styleEl.hasAttribute('data-precedence')) {
-              continue;
-            }
-            break;
-          }
-          case 'SCRIPT': {
-            const scriptEl: HTMLScriptElement = (element: any);
-            if (scriptEl.hasAttribute('async')) {
-              continue;
-            }
-            break;
-          }
-        }
-        break;
-      } else if (nodeType === TEXT_NODE) {
-        break;
-      }
-    } else if (enableFloat) {
-      if (nodeType === ELEMENT_NODE) {
-        const element: Element = (node: any);
-        switch (element.tagName) {
-          case 'TITLE':
-          case 'META': {
-            continue;
-          }
-          case 'LINK': {
-            const linkEl: HTMLLinkElement = (element: any);
-            // All links that are server rendered are resources except
-            // stylesheets that do not have a precedence
-            if (
-              linkEl.rel === 'stylesheet' &&
-              !linkEl.hasAttribute('data-precedence')
-            ) {
-              break;
-            }
-            continue;
-          }
-          case 'STYLE': {
-            const styleEl: HTMLStyleElement = (element: any);
-            if (styleEl.hasAttribute('data-precedence')) {
-              continue;
-            }
-            break;
-          }
-          case 'SCRIPT': {
-            const scriptEl: HTMLScriptElement = (element: any);
-            if (scriptEl.hasAttribute('async')) {
-              continue;
-            }
-            break;
-          }
-        }
-        break;
-      } else if (nodeType === TEXT_NODE) {
-        break;
-      }
-    } else if (enableHostSingletons) {
-      if (nodeType === ELEMENT_NODE) {
-        const tag: string = (node: any).tagName;
-        if (tag === 'HTML' || tag === 'HEAD' || tag === 'BODY') {
-          continue;
-        }
-        break;
-      } else if (nodeType === TEXT_NODE) {
-        break;
-      }
-    } else {
-      if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
-        break;
-      }
+    if (nodeType === ELEMENT_NODE || nodeType === TEXT_NODE) {
+      break;
     }
     if (nodeType === COMMENT_NODE) {
       const nodeData = (node: any).data;
@@ -1093,26 +1125,28 @@ export function hydrateInstance(
   // TODO: Possibly defer this until the commit phase where all the events
   // get attached.
   updateFiberProps(instance, props);
-  let parentNamespace: string;
-  if (__DEV__) {
-    const hostContextDev = ((hostContext: any): HostContextDev);
-    parentNamespace = hostContextDev.namespace;
-  } else {
-    parentNamespace = ((hostContext: any): HostContextProd);
-  }
 
   // TODO: Temporary hack to check if we're in a concurrent root. We can delete
   // when the legacy root API is removed.
   const isConcurrentMode =
     ((internalInstanceHandle: Fiber).mode & ConcurrentMode) !== NoMode;
 
+  let parentNamespace;
+  if (__DEV__) {
+    const hostContextDev = ((hostContext: any): HostContextDev);
+    parentNamespace = hostContextDev.namespace;
+  } else {
+    const hostContextProd = ((hostContext: any): HostContextProd);
+    parentNamespace = hostContextProd;
+  }
+
   return diffHydratedProperties(
     instance,
     type,
     props,
-    parentNamespace,
     isConcurrentMode,
     shouldWarnDev,
+    parentNamespace,
   );
 }
 
@@ -1584,7 +1618,7 @@ export function isHostHoistableType(
   hostContext: HostContext,
 ): boolean {
   let outsideHostContainerContext: boolean;
-  let namespace: string;
+  let namespace: HostContextProd;
   if (__DEV__) {
     const hostContextDev: HostContextDev = (hostContext: any);
     // We can only render resources when we are not within the host container context
@@ -1595,17 +1629,41 @@ export function isHostHoistableType(
     const hostContextProd: HostContextProd = (hostContext: any);
     namespace = hostContextProd;
   }
+
+  // Global opt out of hoisting for anything in SVG Namespace or anything with an itemProp inside an itemScope
+  if (namespace === SVG_NAMESPACE || props.itemProp != null) {
+    if (__DEV__) {
+      if (
+        outsideHostContainerContext &&
+        props.itemProp != null &&
+        (type === 'meta' ||
+          type === 'title' ||
+          type === 'style' ||
+          type === 'link' ||
+          type === 'script')
+      ) {
+        console.error(
+          'Cannot render a <%s> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an' +
+            ' `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <%s> remove the `itemProp` prop.' +
+            ' Otherwise, try moving this tag into the <head> or <body> of the Document.',
+          type,
+          type,
+        );
+      }
+    }
+    return false;
+  }
+
   switch (type) {
     case 'meta':
     case 'title': {
-      return namespace !== SVG_NAMESPACE;
+      return true;
     }
     case 'style': {
       if (
         typeof props.precedence !== 'string' ||
         typeof props.href !== 'string' ||
-        props.href === '' ||
-        namespace === SVG_NAMESPACE
+        props.href === ''
       ) {
         if (__DEV__) {
           if (outsideHostContainerContext) {
@@ -1629,8 +1687,7 @@ export function isHostHoistableType(
         typeof props.href !== 'string' ||
         props.href === '' ||
         props.onLoad ||
-        props.onError ||
-        namespace === SVG_NAMESPACE
+        props.onError
       ) {
         if (__DEV__) {
           if (
@@ -1686,8 +1743,7 @@ export function isHostHoistableType(
         props.onLoad ||
         props.onError ||
         typeof props.src !== 'string' ||
-        !props.src ||
-        namespace === SVG_NAMESPACE
+        !props.src
       ) {
         if (__DEV__) {
           if (outsideHostContainerContext) {
@@ -1771,8 +1827,8 @@ export function resolveSingletonInstance(
   validateDOMNestingDev: boolean,
 ): Instance {
   if (__DEV__) {
+    const hostContextDev = ((hostContext: any): HostContextDev);
     if (validateDOMNestingDev) {
-      const hostContextDev = ((hostContext: any): HostContextDev);
       validateDOMNesting(type, null, hostContextDev.ancestorInfo);
     }
   }
diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
index f3a234305b4df..77a7067e5d413 100644
--- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
+++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
@@ -1257,7 +1257,11 @@ function pushMeta(
   noscriptTagInScope: boolean,
 ): null {
   if (enableFloat) {
-    if (insertionMode === SVG_MODE || noscriptTagInScope) {
+    if (
+      insertionMode === SVG_MODE ||
+      noscriptTagInScope ||
+      props.itemProp != null
+    ) {
       return pushSelfClosing(target, props, 'meta');
     } else {
       if (textEmbedded) {
@@ -1293,6 +1297,7 @@ function pushLink(
     if (
       insertionMode === SVG_MODE ||
       noscriptTagInScope ||
+      props.itemProp != null ||
       typeof rel !== 'string' ||
       typeof href !== 'string' ||
       href === ''
@@ -1573,6 +1578,7 @@ function pushStyle(
     if (
       insertionMode === SVG_MODE ||
       noscriptTagInScope ||
+      props.itemProp != null ||
       typeof precedence !== 'string' ||
       typeof href !== 'string' ||
       href === ''
@@ -1843,7 +1849,11 @@ function pushTitle(
   }
 
   if (enableFloat) {
-    if (insertionMode !== SVG_MODE && !noscriptTagInScope) {
+    if (
+      insertionMode !== SVG_MODE &&
+      !noscriptTagInScope &&
+      props.itemProp == null
+    ) {
       pushTitleImpl(responseState.hoistableChunks, props);
       return null;
     } else {
@@ -2034,6 +2044,7 @@ function pushScript(
     if (
       insertionMode === SVG_MODE ||
       noscriptTagInScope ||
+      props.itemProp != null ||
       typeof props.src !== 'string' ||
       !props.src
     ) {
diff --git a/packages/react-dom-bindings/src/shared/DOMNamespaces.js b/packages/react-dom-bindings/src/shared/DOMNamespaces.js
index 43fdf00865155..6c6c887ffafa5 100644
--- a/packages/react-dom-bindings/src/shared/DOMNamespaces.js
+++ b/packages/react-dom-bindings/src/shared/DOMNamespaces.js
@@ -4,7 +4,7 @@
  * This source code is licensed under the MIT license found in the
  * LICENSE file in the root directory of this source tree.
  *
- * @noflow
+ * @flow
  */
 
 export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
diff --git a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
index ca5222a9b56b4..0d540b9541ffc 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFloat-test.js
@@ -369,7 +369,6 @@ describe('ReactDOMFloat', () => {
           <meta property="foo" content="bar" />
           <title>foo</title>
           <link rel="foo" href="bar" />
-          <link rel="foo" href="bar" />
           <noscript>&lt;link rel="icon" href="icon"/&gt;</noscript>
           <base target="foo" href="bar" />
           <script async="" src="foo" />
@@ -2304,6 +2303,342 @@ body {
     );
   });
 
+  it('does not hoist anything with an itemprop prop', async () => {
+    function App() {
+      return (
+        <html>
+          <head>
+            <meta itemProp="outside" content="unscoped" />
+            <link itemProp="link" rel="foo" href="foo" />
+            <title itemProp="outside-title">title</title>
+            <link
+              itemProp="outside-stylesheet"
+              rel="stylesheet"
+              href="bar"
+              precedence="default"
+            />
+            <style itemProp="outside-style" href="baz" precedence="default">
+              outside style
+            </style>
+            <script itemProp="outside-script" async={true} src="qux" />
+          </head>
+          <body>
+            <div itemScope={true}>
+              <div>
+                <meta itemProp="inside-meta" content="scoped" />
+                <link itemProp="inside-link" rel="foo" href="foo" />
+                <title itemProp="inside-title">title</title>
+                <link
+                  itemProp="inside-stylesheet"
+                  rel="stylesheet"
+                  href="bar"
+                  precedence="default"
+                />
+                <style itemProp="inside-style" href="baz" precedence="default">
+                  inside style
+                </style>
+                <script itemProp="inside-script" async={true} src="qux" />
+              </div>
+            </div>
+          </body>
+        </html>
+      );
+    }
+    await actIntoEmptyDocument(() => {
+      renderToPipeableStream(<App />).pipe(writable);
+    });
+
+    expect(getMeaningfulChildren(document)).toEqual(
+      <html>
+        <head>
+          <meta itemprop="outside" content="unscoped" />
+          <link itemprop="link" rel="foo" href="foo" />
+          <title itemprop="outside-title">title</title>
+          <link
+            itemprop="outside-stylesheet"
+            rel="stylesheet"
+            href="bar"
+            precedence="default"
+          />
+          <style itemprop="outside-style" href="baz" precedence="default">
+            outside style
+          </style>
+          <script itemprop="outside-script" async="" src="qux" />
+        </head>
+        <body>
+          <div itemscope="">
+            <div>
+              <meta itemprop="inside-meta" content="scoped" />
+              <link itemprop="inside-link" rel="foo" href="foo" />
+              <title itemprop="inside-title">title</title>
+              <link
+                itemprop="inside-stylesheet"
+                rel="stylesheet"
+                href="bar"
+                precedence="default"
+              />
+              <style itemprop="inside-style" href="baz" precedence="default">
+                inside style
+              </style>
+              <script itemprop="inside-script" async="" src="qux" />
+            </div>
+          </div>
+        </body>
+      </html>,
+    );
+
+    ReactDOMClient.hydrateRoot(document, <App />);
+    expect(Scheduler).toFlushWithoutYielding();
+
+    expect(getMeaningfulChildren(document)).toEqual(
+      <html>
+        <head>
+          <meta itemprop="outside" content="unscoped" />
+          <link itemprop="link" rel="foo" href="foo" />
+          <title itemprop="outside-title">title</title>
+          <link
+            itemprop="outside-stylesheet"
+            rel="stylesheet"
+            href="bar"
+            precedence="default"
+          />
+          <style itemprop="outside-style" href="baz" precedence="default">
+            outside style
+          </style>
+          <script itemprop="outside-script" async="" src="qux" />
+        </head>
+        <body>
+          <div itemscope="">
+            <div>
+              <meta itemprop="inside-meta" content="scoped" />
+              <link itemprop="inside-link" rel="foo" href="foo" />
+              <title itemprop="inside-title">title</title>
+              <link
+                itemprop="inside-stylesheet"
+                rel="stylesheet"
+                href="bar"
+                precedence="default"
+              />
+              <style itemprop="inside-style" href="baz" precedence="default">
+                inside style
+              </style>
+              <script itemprop="inside-script" async="" src="qux" />
+            </div>
+          </div>
+        </body>
+      </html>,
+    );
+  });
+
+  it('warns if you render a tag with itemProp outside <body> or <head>', async () => {
+    const root = ReactDOMClient.createRoot(document);
+    root.render(
+      <html>
+        <meta itemProp="foo" />
+        <title itemProp="foo">title</title>
+        <style itemProp="foo">style</style>
+        <link itemProp="foo" />
+        <script itemProp="foo" />
+      </html>,
+    );
+    expect(() => {
+      expect(Scheduler).toFlushWithoutYielding();
+    }).toErrorDev([
+      'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
+      'Cannot render a <title> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <title> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
+      'Cannot render a <style> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <style> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
+      'Cannot render a <link> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <link> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
+      'Cannot render a <script> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <script> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
+      'validateDOMNesting(...): <meta> cannot appear as a child of <html>',
+      'validateDOMNesting(...): <title> cannot appear as a child of <html>',
+      'validateDOMNesting(...): <style> cannot appear as a child of <html>',
+      'validateDOMNesting(...): <link> cannot appear as a child of <html>',
+      'validateDOMNesting(...): <script> cannot appear as a child of <html>',
+    ]);
+  });
+
+  // @gate enableFloat
+  it('can hydrate resources and components in the head and body even if a browser or 3rd party script injects extra html nodes', async () => {
+    // This is a stress test case for hydrating a complex combination of hoistable elements, hoistable resources and host components
+    // in an environment that has been manipulated by 3rd party scripts/extensions to modify the <head> and <body>
+    function App() {
+      return (
+        <>
+          <link rel="foo" href="foo" />
+          <script async={true} src="rendered" />
+          <link rel="stylesheet" href="stylesheet" precedence="default" />
+          <html itemScope={true}>
+            <head>
+              {/* Component */}
+              <link rel="stylesheet" href="stylesheet" />
+              <script src="sync rendered" data-meaningful="" />
+              <style>{'body { background-color: red; }'}</style>
+              <script src="async rendered" async={true} onLoad={() => {}} />
+              <noscript>
+                <meta name="noscript" content="noscript" />
+              </noscript>
+              <link rel="foo" href="foo" onLoad={() => {}} />
+            </head>
+            <body>
+              {/* Component because it has itemProp */}
+              <meta name="foo" content="foo" itemProp="a prop" />
+              {/* regular Hoistable */}
+              <meta name="foo" content="foo" />
+              {/* regular Hoistable */}
+              <title>title</title>
+              <div itemScope={true}>
+                <div>
+                  <div>deep hello</div>
+                  {/* Component because it has itemProp */}
+                  <meta name="foo" content="foo" itemProp="a prop" />
+                </div>
+              </div>
+            </body>
+          </html>
+          <link rel="foo" href="foo" />
+        </>
+      );
+    }
+
+    await actIntoEmptyDocument(() => {
+      renderToPipeableStream(<App />).pipe(writable);
+    });
+
+    expect(getMeaningfulChildren(document)).toEqual(
+      <html itemscope="">
+        <head>
+          {/* Hoisted Resources and elements */}
+          <link rel="stylesheet" href="stylesheet" data-precedence="default" />
+          <script async="" src="rendered" />
+          <link rel="preload" as="script" href="sync rendered" />
+          <link rel="preload" as="script" href="async rendered" />
+          <link rel="foo" href="foo" />
+          <meta name="foo" content="foo" />
+          <title>title</title>
+          <link rel="foo" href="foo" />
+          {/* rendered host components */}
+          <link rel="stylesheet" href="stylesheet" />
+          <script src="sync rendered" data-meaningful="" />
+          <style>{'body { background-color: red; }'}</style>
+          <noscript>&lt;meta name="noscript" content="noscript"/&gt;</noscript>
+          <link rel="foo" href="foo" />
+        </head>
+        <body>
+          <meta name="foo" content="foo" itemprop="a prop" />
+          <div itemscope="">
+            <div>
+              <div>deep hello</div>
+              <meta name="foo" content="foo" itemprop="a prop" />
+            </div>
+          </div>
+        </body>
+      </html>,
+    );
+
+    // We inject some styles, divs, scripts into the begginning, middle, and end
+    // of the head / body.
+    const injectedStyle = document.createElement('style');
+    injectedStyle.textContent = 'body { background-color: blue; }';
+    document.head.prepend(injectedStyle.cloneNode(true));
+    document.head.appendChild(injectedStyle.cloneNode(true));
+    document.body.prepend(injectedStyle.cloneNode(true));
+    document.body.appendChild(injectedStyle.cloneNode(true));
+
+    const injectedDiv = document.createElement('div');
+    document.head.prepend(injectedDiv);
+    document.head.appendChild(injectedDiv.cloneNode(true));
+    // We do not prepend a <div> in body because this will conflict with hyration
+    // We still mostly hydrate by matchign tag and <div> does not have any attributes to
+    // differentiate between likely-inject and likely-rendered cases. If a <div> is prepended
+    // in the <body> and you render a <div> as the first child of <body> there will be a conflict.
+    // We consider this a rare edge case and even if it does happen the fallback to client rendering
+    // should patch up the DOM correctly
+    document.body.appendChild(injectedDiv.cloneNode(true));
+
+    const injectedScript = document.createElement('script');
+    injectedScript.setAttribute('async', '');
+    injectedScript.setAttribute('src', 'injected');
+    document.head.prepend(injectedScript);
+    document.head.appendChild(injectedScript.cloneNode(true));
+    document.body.prepend(injectedScript.cloneNode(true));
+    document.body.appendChild(injectedScript.cloneNode(true));
+
+    // We hydrate the same App and confirm the output is identical except for the async
+    // script insertion that happens because we do not SSR async scripts with load handlers.
+    // All the extra inject nodes are preset
+    const root = ReactDOMClient.hydrateRoot(document, <App />);
+    expect(Scheduler).toFlushWithoutYielding();
+    expect(getMeaningfulChildren(document)).toEqual(
+      <html itemscope="">
+        <head>
+          <script async="" src="injected" />
+          <div />
+          <style>{'body { background-color: blue; }'}</style>
+          <link rel="stylesheet" href="stylesheet" data-precedence="default" />
+          <script async="" src="rendered" />
+          <link rel="preload" as="script" href="sync rendered" />
+          <link rel="preload" as="script" href="async rendered" />
+          <link rel="foo" href="foo" />
+          <meta name="foo" content="foo" />
+          <title>title</title>
+          <link rel="foo" href="foo" />
+          <link rel="stylesheet" href="stylesheet" />
+          <script src="sync rendered" data-meaningful="" />
+          <style>{'body { background-color: red; }'}</style>
+          <script src="async rendered" async="" />
+          <noscript>&lt;meta name="noscript" content="noscript"/&gt;</noscript>
+          <link rel="foo" href="foo" />
+          <style>{'body { background-color: blue; }'}</style>
+          <div />
+          <script async="" src="injected" />
+        </head>
+        <body>
+          <script async="" src="injected" />
+          <style>{'body { background-color: blue; }'}</style>
+          <meta name="foo" content="foo" itemprop="a prop" />
+          <div itemscope="">
+            <div>
+              <div>deep hello</div>
+              <meta name="foo" content="foo" itemprop="a prop" />
+            </div>
+          </div>
+          <style>{'body { background-color: blue; }'}</style>
+          <div />
+          <script async="" src="injected" />
+        </body>
+      </html>,
+    );
+
+    // We unmount. The nodes that remain are
+    // 1. Hoisted resources (we don't clean these up on unmount to address races with streaming suspense and navigation)
+    // 2. preloads that are injected to hint the browser to load a resource but are not associated to Fibers directly
+    // 3. Nodes that React skipped over during hydration
+    root.unmount();
+    expect(getMeaningfulChildren(document)).toEqual(
+      <html>
+        <head>
+          <script async="" src="injected" />
+          <div />
+          <style>{'body { background-color: blue; }'}</style>
+          <link rel="stylesheet" href="stylesheet" data-precedence="default" />
+          <script async="" src="rendered" />
+          <link rel="preload" as="script" href="sync rendered" />
+          <link rel="preload" as="script" href="async rendered" />
+          <style>{'body { background-color: blue; }'}</style>
+          <div />
+          <script async="" src="injected" />
+        </head>
+        <body>
+          <script async="" src="injected" />
+          <style>{'body { background-color: blue; }'}</style>
+          <style>{'body { background-color: blue; }'}</style>
+          <div />
+          <script async="" src="injected" />
+        </body>
+      </html>,
+    );
+  });
+
   describe('ReactDOM.prefetchDNS(href)', () => {
     it('creates a dns-prefetch resource when called', async () => {
       function App({url}) {
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
index e4d9e5806396a..24ce917a18d5a 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
@@ -3380,7 +3380,6 @@ describe('ReactDOMServerPartialHydration', () => {
   itHydratesWithoutMismatch('an empty string in class component', TestAppClass);
 
   it('fallback to client render on hydration mismatch at root', async () => {
-    let isClient = false;
     let suspend = true;
     let resolve;
     const promise = new Promise((res, rej) => {
@@ -3389,36 +3388,35 @@ describe('ReactDOMServerPartialHydration', () => {
         res();
       };
     });
-    function App() {
+    function App({isClient}) {
       return (
         <>
           <Suspense fallback={<div>Loading</div>}>
-            <ChildThatSuspends id={1} />
+            <ChildThatSuspends id={1} isClient={isClient} />
           </Suspense>
           {isClient ? <span>client</span> : <div>server</div>}
           <Suspense fallback={<div>Loading</div>}>
-            <ChildThatSuspends id={2} />
+            <ChildThatSuspends id={2} isClient={isClient} />
           </Suspense>
         </>
       );
     }
-    function ChildThatSuspends({id}) {
+    function ChildThatSuspends({id, isClient}) {
       if (isClient && suspend) {
         throw promise;
       }
       return <div>{id}</div>;
     }
 
-    const finalHTML = ReactDOMServer.renderToString(<App />);
+    const finalHTML = ReactDOMServer.renderToString(<App isClient={false} />);
 
     const container = document.createElement('div');
     document.body.appendChild(container);
     container.innerHTML = finalHTML;
-    isClient = true;
 
     expect(() => {
       act(() => {
-        ReactDOMClient.hydrateRoot(container, <App />, {
+        ReactDOMClient.hydrateRoot(container, <App isClient={true} />, {
           onRecoverableError(error) {
             Scheduler.log('Log recoverable error: ' + error.message);
           },
@@ -3435,9 +3433,6 @@ describe('ReactDOMServerPartialHydration', () => {
       {withoutStack: 1},
     );
     assertLog([
-      'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
-      // TODO: There were multiple mismatches in a single container. Should
-      // we attempt to de-dupe them?
       'Log recoverable error: Hydration failed because the initial UI does not match what was rendered on the server.',
       'Log recoverable error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.',
     ]);
diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js
index e3b08fb1dcb01..4126a7a324c2f 100644
--- a/packages/react-reconciler/src/ReactFiberBeginWork.js
+++ b/packages/react-reconciler/src/ReactFiberBeginWork.js
@@ -227,6 +227,8 @@ import {
   resetHydrationState,
   claimHydratableSingleton,
   tryToClaimNextHydratableInstance,
+  tryToClaimNextHydratableTextInstance,
+  tryToClaimNextHydratableSuspenseInstance,
   warnIfHydrating,
   queueHydrationError,
 } from './ReactFiberHydrationContext';
@@ -1694,7 +1696,7 @@ function updateHostSingleton(
 
 function updateHostText(current: null | Fiber, workInProgress: Fiber) {
   if (current === null) {
-    tryToClaimNextHydratableInstance(workInProgress);
+    tryToClaimNextHydratableTextInstance(workInProgress);
   }
   // Nothing to do here. This is terminal. We'll do the completion step
   // immediately after.
@@ -2250,7 +2252,7 @@ function updateSuspenseComponent(
       } else {
         pushFallbackTreeSuspenseHandler(workInProgress);
       }
-      tryToClaimNextHydratableInstance(workInProgress);
+      tryToClaimNextHydratableSuspenseInstance(workInProgress);
       // This could've been a dehydrated suspense component.
       const suspenseState: null | SuspenseState = workInProgress.memoizedState;
       if (suspenseState !== null) {
diff --git a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
index 26737c8d24fe9..93f82b1965dc9 100644
--- a/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
+++ b/packages/react-reconciler/src/ReactFiberHostConfigWithNoHydration.js
@@ -21,10 +21,8 @@ function shim(...args: any): empty {
 // Hydration (when unsupported)
 export type SuspenseInstance = mixed;
 export const supportsHydration = false;
-export const isHydratable = shim;
-export const canHydrateInstance = shim;
-export const canHydrateTextInstance = shim;
-export const canHydrateSuspenseInstance = shim;
+export const isHydratableType = shim;
+export const isHydratableText = shim;
 export const isSuspenseInstancePending = shim;
 export const isSuspenseInstanceFallback = shim;
 export const getSuspenseInstanceFallbackErrorDetails = shim;
@@ -33,6 +31,12 @@ export const getNextHydratableSibling = shim;
 export const getFirstHydratableChild = shim;
 export const getFirstHydratableChildWithinContainer = shim;
 export const getFirstHydratableChildWithinSuspenseInstance = shim;
+export const shouldSkipHydratableForInstance = shim;
+export const shouldSkipHydratableForTextInstance = shim;
+export const shouldSkipHydratableForSuspenseInstance = shim;
+export const canHydrateInstance = shim;
+export const canHydrateTextInstance = shim;
+export const canHydrateSuspenseInstance = shim;
 export const hydrateInstance = shim;
 export const hydrateTextInstance = shim;
 export const hydrateSuspenseInstance = shim;
diff --git a/packages/react-reconciler/src/ReactFiberHostContext.js b/packages/react-reconciler/src/ReactFiberHostContext.js
index ed1cfa92cddb0..539dedbbd1c8b 100644
--- a/packages/react-reconciler/src/ReactFiberHostContext.js
+++ b/packages/react-reconciler/src/ReactFiberHostContext.js
@@ -40,7 +40,7 @@ function getRootHostContainer(): Container {
   return rootInstance;
 }
 
-function pushHostContainer(fiber: Fiber, nextRootInstance: Container) {
+function pushHostContainer(fiber: Fiber, nextRootInstance: Container): void {
   // Push current root instance onto the stack;
   // This allows us to reset root when portals are popped.
   push(rootInstanceStackCursor, nextRootInstance, fiber);
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js
index fa6bb47298851..fbf74de14396b 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js
@@ -45,10 +45,6 @@ import {
   shouldSetTextContent,
   supportsHydration,
   supportsSingletons,
-  isHydratable,
-  canHydrateInstance,
-  canHydrateTextInstance,
-  canHydrateSuspenseInstance,
   getNextHydratableSibling,
   getFirstHydratableChild,
   getFirstHydratableChildWithinContainer,
@@ -73,6 +69,14 @@ import {
   didNotFindHydratableTextInstance,
   didNotFindHydratableSuspenseInstance,
   resolveSingletonInstance,
+  shouldSkipHydratableForInstance,
+  shouldSkipHydratableForTextInstance,
+  shouldSkipHydratableForSuspenseInstance,
+  canHydrateInstance,
+  canHydrateTextInstance,
+  canHydrateSuspenseInstance,
+  isHydratableType,
+  isHydratableText,
 } from './ReactFiberHostConfig';
 import {OffscreenLane} from './ReactFiberLane';
 import {
@@ -95,6 +99,8 @@ let didSuspendOrErrorDEV: boolean = false;
 // Hydration errors that were thrown inside this boundary
 let hydrationErrors: Array<CapturedValue<mixed>> | null = null;
 
+let rootOrSingletonContext = false;
+
 function warnIfHydrating() {
   if (__DEV__) {
     if (isHydrating) {
@@ -130,6 +136,7 @@ function enterHydrationState(fiber: Fiber): boolean {
   isHydrating = true;
   hydrationErrors = null;
   didSuspendOrErrorDEV = false;
+  rootOrSingletonContext = true;
   return true;
 }
 
@@ -147,6 +154,7 @@ function reenterHydrationStateFromDehydratedSuspenseInstance(
   isHydrating = true;
   hydrationErrors = null;
   didSuspendOrErrorDEV = false;
+  rootOrSingletonContext = false;
   if (treeContext !== null) {
     restoreSuspendedTreeContext(fiber, treeContext);
   }
@@ -336,63 +344,62 @@ function insertNonHydratedInstance(returnFiber: Fiber, fiber: Fiber) {
   warnNonhydratedInstance(returnFiber, fiber);
 }
 
-function tryHydrate(fiber: Fiber, nextInstance: any) {
-  switch (fiber.tag) {
-    // HostSingleton is intentionally omitted. the hydration pathway for singletons is non-fallible
-    // you can find it inlined in claimHydratableSingleton
-    case HostComponent: {
-      const type = fiber.type;
-      const props = fiber.pendingProps;
-      const instance = canHydrateInstance(nextInstance, type, props);
-      if (instance !== null) {
-        fiber.stateNode = (instance: Instance);
-        hydrationParentFiber = fiber;
-        nextHydratableInstance = getFirstHydratableChild(instance);
-        return true;
-      }
-      return false;
-    }
-    case HostText: {
-      const text = fiber.pendingProps;
-      const textInstance = canHydrateTextInstance(nextInstance, text);
-      if (textInstance !== null) {
-        fiber.stateNode = (textInstance: TextInstance);
-        hydrationParentFiber = fiber;
-        // Text Instances don't have children so there's nothing to hydrate.
-        nextHydratableInstance = null;
-        return true;
-      }
-      return false;
-    }
-    case SuspenseComponent: {
-      const suspenseInstance: null | SuspenseInstance =
-        canHydrateSuspenseInstance(nextInstance);
-      if (suspenseInstance !== null) {
-        const suspenseState: SuspenseState = {
-          dehydrated: suspenseInstance,
-          treeContext: getSuspendedTreeContext(),
-          retryLane: OffscreenLane,
-        };
-        fiber.memoizedState = suspenseState;
-        // Store the dehydrated fragment as a child fiber.
-        // This simplifies the code for getHostSibling and deleting nodes,
-        // since it doesn't have to consider all Suspense boundaries and
-        // check if they're dehydrated ones or not.
-        const dehydratedFragment =
-          createFiberFromDehydratedFragment(suspenseInstance);
-        dehydratedFragment.return = fiber;
-        fiber.child = dehydratedFragment;
-        hydrationParentFiber = fiber;
-        // While a Suspense Instance does have children, we won't step into
-        // it during the first pass. Instead, we'll reenter it later.
-        nextHydratableInstance = null;
-        return true;
-      }
-      return false;
-    }
-    default:
-      return false;
+function tryHydrateInstance(fiber: Fiber, nextInstance: any) {
+  // fiber is a HostComponent Fiber
+  const instance = canHydrateInstance(
+    nextInstance,
+    fiber.type,
+    fiber.pendingProps,
+  );
+  if (instance !== null) {
+    fiber.stateNode = (instance: Instance);
+    hydrationParentFiber = fiber;
+    nextHydratableInstance = getFirstHydratableChild(instance);
+    rootOrSingletonContext = false;
+    return true;
   }
+  return false;
+}
+
+function tryHydrateText(fiber: Fiber, nextInstance: any) {
+  // fiber is a HostText Fiber
+  const text = fiber.pendingProps;
+  const textInstance = canHydrateTextInstance(nextInstance, text);
+  if (textInstance !== null) {
+    fiber.stateNode = (textInstance: TextInstance);
+    hydrationParentFiber = fiber;
+    // Text Instances don't have children so there's nothing to hydrate.
+    nextHydratableInstance = null;
+    return true;
+  }
+  return false;
+}
+
+function tryHydrateSuspense(fiber: Fiber, nextInstance: any) {
+  // fiber is a SuspenseComponent Fiber
+  const suspenseInstance = canHydrateSuspenseInstance(nextInstance);
+  if (suspenseInstance !== null) {
+    const suspenseState: SuspenseState = {
+      dehydrated: suspenseInstance,
+      treeContext: getSuspendedTreeContext(),
+      retryLane: OffscreenLane,
+    };
+    fiber.memoizedState = suspenseState;
+    // Store the dehydrated fragment as a child fiber.
+    // This simplifies the code for getHostSibling and deleting nodes,
+    // since it doesn't have to consider all Suspense boundaries and
+    // check if they're dehydrated ones or not.
+    const dehydratedFragment =
+      createFiberFromDehydratedFragment(suspenseInstance);
+    dehydratedFragment.return = fiber;
+    fiber.child = dehydratedFragment;
+    hydrationParentFiber = fiber;
+    // While a Suspense Instance does have children, we won't step into
+    // it during the first pass. Instead, we'll reenter it later.
+    nextHydratableInstance = null;
+    return true;
+  }
+  return false;
 }
 
 function shouldClientRenderOnMismatch(fiber: Fiber) {
@@ -424,22 +431,189 @@ function claimHydratableSingleton(fiber: Fiber): void {
       false,
     ));
     hydrationParentFiber = fiber;
+    rootOrSingletonContext = true;
     nextHydratableInstance = getFirstHydratableChild(instance);
   }
 }
 
+function advanceToFirstAttempableInstance(fiber: Fiber) {
+  // fiber is HostComponent Fiber
+  while (
+    nextHydratableInstance &&
+    shouldSkipHydratableForInstance(
+      nextHydratableInstance,
+      fiber.type,
+      fiber.pendingProps,
+    )
+  ) {
+    // Flow doesn't understand that inside this block nextHydratableInstance is not null
+    const instance: HydratableInstance = (nextHydratableInstance: any);
+    nextHydratableInstance = getNextHydratableSibling(instance);
+  }
+}
+
+function advanceToFirstAttempableTextInstance() {
+  while (
+    nextHydratableInstance &&
+    shouldSkipHydratableForTextInstance(nextHydratableInstance)
+  ) {
+    // Flow doesn't understand that inside this block nextHydratableInstance is not null
+    const instance: HydratableInstance = (nextHydratableInstance: any);
+    nextHydratableInstance = getNextHydratableSibling(instance);
+  }
+}
+
+function advanceToFirstAttempableSuspenseInstance() {
+  while (
+    nextHydratableInstance &&
+    shouldSkipHydratableForSuspenseInstance(nextHydratableInstance)
+  ) {
+    // Flow doesn't understand that inside this block nextHydratableInstance is not null
+    const instance: HydratableInstance = (nextHydratableInstance: any);
+    nextHydratableInstance = getNextHydratableSibling(instance);
+  }
+}
+
 function tryToClaimNextHydratableInstance(fiber: Fiber): void {
   if (!isHydrating) {
     return;
   }
-  if (enableFloat && !isHydratable(fiber.type, fiber.pendingProps)) {
-    // This fiber never hydrates from the DOM and always does an insert
-    fiber.flags = (fiber.flags & ~Hydrating) | Placement;
+  if (enableFloat) {
+    if (!isHydratableType(fiber.type, fiber.pendingProps)) {
+      // This fiber never hydrates from the DOM and always does an insert
+      fiber.flags = (fiber.flags & ~Hydrating) | Placement;
+      isHydrating = false;
+      hydrationParentFiber = fiber;
+      return;
+    }
+  }
+  const initialInstance = nextHydratableInstance;
+  if (rootOrSingletonContext) {
+    // We may need to skip past certain nodes in these contexts
+    advanceToFirstAttempableInstance(fiber);
+  }
+  const nextInstance = nextHydratableInstance;
+  if (!nextInstance) {
+    if (shouldClientRenderOnMismatch(fiber)) {
+      warnNonhydratedInstance((hydrationParentFiber: any), fiber);
+      throwOnHydrationMismatch(fiber);
+    }
+    // Nothing to hydrate. Make it an insertion.
+    insertNonHydratedInstance((hydrationParentFiber: any), fiber);
     isHydrating = false;
     hydrationParentFiber = fiber;
+    nextHydratableInstance = initialInstance;
     return;
   }
-  let nextInstance = nextHydratableInstance;
+  const firstAttemptedInstance = nextInstance;
+  if (!tryHydrateInstance(fiber, nextInstance)) {
+    if (shouldClientRenderOnMismatch(fiber)) {
+      warnNonhydratedInstance((hydrationParentFiber: any), fiber);
+      throwOnHydrationMismatch(fiber);
+    }
+    // If we can't hydrate this instance let's try the next one.
+    // We use this as a heuristic. It's based on intuition and not data so it
+    // might be flawed or unnecessary.
+    nextHydratableInstance = getNextHydratableSibling(nextInstance);
+    const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
+    if (rootOrSingletonContext) {
+      // We may need to skip past certain nodes in these contexts
+      advanceToFirstAttempableInstance(fiber);
+    }
+    if (
+      !nextHydratableInstance ||
+      !tryHydrateInstance(fiber, nextHydratableInstance)
+    ) {
+      // Nothing to hydrate. Make it an insertion.
+      insertNonHydratedInstance((hydrationParentFiber: any), fiber);
+      isHydrating = false;
+      hydrationParentFiber = fiber;
+      nextHydratableInstance = initialInstance;
+      return;
+    }
+    // We matched the next one, we'll now assume that the first one was
+    // superfluous and we'll delete it. Since we can't eagerly delete it
+    // we'll have to schedule a deletion. To do that, this node needs a dummy
+    // fiber associated with it.
+    deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance);
+  }
+}
+
+function tryToClaimNextHydratableTextInstance(fiber: Fiber): void {
+  if (!isHydrating) {
+    return;
+  }
+  const text = fiber.pendingProps;
+  const isHydratable = isHydratableText(text);
+
+  const initialInstance = nextHydratableInstance;
+  if (rootOrSingletonContext && isHydratable) {
+    // We may need to skip past certain nodes in these contexts.
+    // We don't skip if the text is not hydratable because we know no hydratables
+    // exist which could match this Fiber
+    advanceToFirstAttempableTextInstance();
+  }
+  const nextInstance = nextHydratableInstance;
+  if (!nextInstance || !isHydratable) {
+    // We exclude non hydrabable text because we know there are no matching hydratables.
+    // We either throw or insert depending on the render mode.
+    if (shouldClientRenderOnMismatch(fiber)) {
+      warnNonhydratedInstance((hydrationParentFiber: any), fiber);
+      throwOnHydrationMismatch(fiber);
+    }
+    // Nothing to hydrate. Make it an insertion.
+    insertNonHydratedInstance((hydrationParentFiber: any), fiber);
+    isHydrating = false;
+    hydrationParentFiber = fiber;
+    nextHydratableInstance = initialInstance;
+    return;
+  }
+  const firstAttemptedInstance = nextInstance;
+  if (!tryHydrateText(fiber, nextInstance)) {
+    if (shouldClientRenderOnMismatch(fiber)) {
+      warnNonhydratedInstance((hydrationParentFiber: any), fiber);
+      throwOnHydrationMismatch(fiber);
+    }
+    // If we can't hydrate this instance let's try the next one.
+    // We use this as a heuristic. It's based on intuition and not data so it
+    // might be flawed or unnecessary.
+    nextHydratableInstance = getNextHydratableSibling(nextInstance);
+    const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
+
+    if (rootOrSingletonContext && isHydratable) {
+      // We may need to skip past certain nodes in these contexts
+      advanceToFirstAttempableTextInstance();
+    }
+
+    if (
+      !nextHydratableInstance ||
+      !tryHydrateText(fiber, nextHydratableInstance)
+    ) {
+      // Nothing to hydrate. Make it an insertion.
+      insertNonHydratedInstance((hydrationParentFiber: any), fiber);
+      isHydrating = false;
+      hydrationParentFiber = fiber;
+      nextHydratableInstance = initialInstance;
+      return;
+    }
+    // We matched the next one, we'll now assume that the first one was
+    // superfluous and we'll delete it. Since we can't eagerly delete it
+    // we'll have to schedule a deletion. To do that, this node needs a dummy
+    // fiber associated with it.
+    deleteHydratableInstance(prevHydrationParentFiber, firstAttemptedInstance);
+  }
+}
+
+function tryToClaimNextHydratableSuspenseInstance(fiber: Fiber): void {
+  if (!isHydrating) {
+    return;
+  }
+  const initialInstance = nextHydratableInstance;
+  if (rootOrSingletonContext) {
+    // We may need to skip past certain nodes in these contexts
+    advanceToFirstAttempableSuspenseInstance();
+  }
+  const nextInstance = nextHydratableInstance;
   if (!nextInstance) {
     if (shouldClientRenderOnMismatch(fiber)) {
       warnNonhydratedInstance((hydrationParentFiber: any), fiber);
@@ -449,10 +623,11 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
     insertNonHydratedInstance((hydrationParentFiber: any), fiber);
     isHydrating = false;
     hydrationParentFiber = fiber;
+    nextHydratableInstance = initialInstance;
     return;
   }
   const firstAttemptedInstance = nextInstance;
-  if (!tryHydrate(fiber, nextInstance)) {
+  if (!tryHydrateSuspense(fiber, nextInstance)) {
     if (shouldClientRenderOnMismatch(fiber)) {
       warnNonhydratedInstance((hydrationParentFiber: any), fiber);
       throwOnHydrationMismatch(fiber);
@@ -460,13 +635,23 @@ function tryToClaimNextHydratableInstance(fiber: Fiber): void {
     // If we can't hydrate this instance let's try the next one.
     // We use this as a heuristic. It's based on intuition and not data so it
     // might be flawed or unnecessary.
-    nextInstance = getNextHydratableSibling(firstAttemptedInstance);
+    nextHydratableInstance = getNextHydratableSibling(nextInstance);
     const prevHydrationParentFiber: Fiber = (hydrationParentFiber: any);
-    if (!nextInstance || !tryHydrate(fiber, nextInstance)) {
+
+    if (rootOrSingletonContext) {
+      // We may need to skip past certain nodes in these contexts
+      advanceToFirstAttempableSuspenseInstance();
+    }
+
+    if (
+      !nextHydratableInstance ||
+      !tryHydrateSuspense(fiber, nextHydratableInstance)
+    ) {
       // Nothing to hydrate. Make it an insertion.
       insertNonHydratedInstance((hydrationParentFiber: any), fiber);
       isHydrating = false;
       hydrationParentFiber = fiber;
+      nextHydratableInstance = initialInstance;
       return;
     }
     // We matched the next one, we'll now assume that the first one was
@@ -616,19 +801,21 @@ function skipPastDehydratedSuspenseInstance(
 }
 
 function popToNextHostParent(fiber: Fiber): void {
-  let parent = fiber.return;
-  while (
-    parent !== null &&
-    parent.tag !== HostComponent &&
-    parent.tag !== HostRoot &&
-    parent.tag !== SuspenseComponent &&
-    (!(enableHostSingletons && supportsSingletons)
-      ? true
-      : parent.tag !== HostSingleton)
-  ) {
-    parent = parent.return;
+  hydrationParentFiber = fiber.return;
+  while (hydrationParentFiber) {
+    switch (hydrationParentFiber.tag) {
+      case HostRoot:
+      case HostSingleton:
+        rootOrSingletonContext = true;
+        return;
+      case HostComponent:
+      case SuspenseComponent:
+        rootOrSingletonContext = false;
+        return;
+      default:
+        hydrationParentFiber = hydrationParentFiber.return;
+    }
   }
-  hydrationParentFiber = parent;
 }
 
 function popHydrationState(fiber: Fiber): boolean {
@@ -755,6 +942,8 @@ export {
   resetHydrationState,
   claimHydratableSingleton,
   tryToClaimNextHydratableInstance,
+  tryToClaimNextHydratableTextInstance,
+  tryToClaimNextHydratableSuspenseInstance,
   prepareToHydrateHostInstance,
   prepareToHydrateHostTextInstance,
   prepareToHydrateHostSuspenseInstance,
diff --git a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
index e42a8bb2841f8..96ba80f601d47 100644
--- a/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberHostConfig.custom.js
@@ -130,11 +130,8 @@ export const cloneHiddenTextInstance = $$$hostConfig.cloneHiddenTextInstance;
 //     Hydration
 //     (optional)
 // -------------------
-export const isHydratable = $$$hostConfig.isHydratable;
-export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
-export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
-export const canHydrateSuspenseInstance =
-  $$$hostConfig.canHydrateSuspenseInstance;
+export const isHydratableType = $$$hostConfig.isHydratableType;
+export const isHydratableText = $$$hostConfig.isHydratableText;
 export const isSuspenseInstancePending =
   $$$hostConfig.isSuspenseInstancePending;
 export const isSuspenseInstanceFallback =
@@ -149,6 +146,16 @@ export const getFirstHydratableChildWithinContainer =
   $$$hostConfig.getFirstHydratableChildWithinContainer;
 export const getFirstHydratableChildWithinSuspenseInstance =
   $$$hostConfig.getFirstHydratableChildWithinSuspenseInstance;
+export const shouldSkipHydratableForInstance =
+  $$$hostConfig.shouldSkipHydratableForInstance;
+export const shouldSkipHydratableForTextInstance =
+  $$$hostConfig.shouldSkipHydratableForTextInstance;
+export const shouldSkipHydratableForSuspenseInstance =
+  $$$hostConfig.shouldSkipHydratableForSuspenseInstance;
+export const canHydrateInstance = $$$hostConfig.canHydrateInstance;
+export const canHydrateTextInstance = $$$hostConfig.canHydrateTextInstance;
+export const canHydrateSuspenseInstance =
+  $$$hostConfig.canHydrateSuspenseInstance;
 export const hydrateInstance = $$$hostConfig.hydrateInstance;
 export const hydrateTextInstance = $$$hostConfig.hydrateTextInstance;
 export const hydrateSuspenseInstance = $$$hostConfig.hydrateSuspenseInstance;