Repository: facebook/react
Pull request: Change DOM HostContext to number instead of string

In React DOM, we use HostContext to represent the namespace of whatever is currently rendering — SVG, Math, or HTML. Because there is a fixed set of possible values, we can switch this to be a number instead. My motivation is that I want to start tracking additional information in this type, and I want to pack all of it into a single number instead of turning it into an object. For better performance.

(In dev, the host context type is already an object that includes additional information, but that's dev so who cares.)

Technically, before this change, the host context could be any namespace URI string, but any value other than SVG or Math was treated the same way. Only SVG and Math have special behavior. So in the new structure, there are three enum values: SVG, Math, or None, which represents the HTML namespace as well as all other possible namespaces.

Changed files:
filename: packages/react-dom-bindings/src/client/DOMNamespaces.js
patch: @@ -7,34 +7,5 @@
  * @flow
  */
 
-export const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';
 export const MATH_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';
 export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
-
-// Assumes there is no parent namespace.
-export function getIntrinsicNamespace(type: string): string {
-  switch (type) {
-    case 'svg':
-      return SVG_NAMESPACE;
-    case 'math':
-      return MATH_NAMESPACE;
-    default:
-      return HTML_NAMESPACE;
-  }
-}
-
-export function getChildNamespace(
-  parentNamespace: string | null,
-  type: string,
-): string {
-  if (parentNamespace == null || parentNamespace === HTML_NAMESPACE) {
-    // No (or default) parent namespace: potential entry point.
-    return getIntrinsicNamespace(type);
-  }
-  if (parentNamespace === SVG_NAMESPACE && type === 'foreignObject') {
-    // We're leaving SVG.
-    return HTML_NAMESPACE;
-  }
-  // By default, pass namespace below.
-  return parentNamespace;
-}

filename: packages/react-dom-bindings/src/client/ReactDOMComponent.js
patch: @@ -7,6 +7,10 @@
  * @flow
  */
 
+import type {HostContext, HostContextDev} from './ReactFiberConfigDOM';
+
+import {HostContextNamespaceNone} from './ReactFiberConfigDOM';
+
 import {
   registrationNameDependencies,
   possibleRegistrationNames,
@@ -53,7 +57,7 @@ import {
   setValueForStyles,
   validateShorthandPropertyCollisionInDev,
 } from './CSSPropertyOperations';
-import {HTML_NAMESPACE, getIntrinsicNamespace} from './DOMNamespaces';
+import {SVG_NAMESPACE, MATH_NAMESPACE} from './DOMNamespaces';
 import isCustomElement from '../shared/isCustomElement';
 import getAttributeAlias from '../shared/getAttributeAlias';
 import possibleStandardNames from '../shared/possibleStandardNames';
@@ -290,12 +294,13 @@ function normalizeHTML(parent: Element, html: string) {
     // how <noscript> is being handled. So we use the same document.
     // See the discussion in https://github.com/facebook/react/pull/11157.
     const testElement =
-      parent.namespaceURI === HTML_NAMESPACE
-        ? parent.ownerDocument.createElement(parent.tagName)
-        : parent.ownerDocument.createElementNS(
+      parent.namespaceURI === MATH_NAMESPACE ||
+      parent.namespaceURI === SVG_NAMESPACE
+        ? parent.ownerDocument.createElementNS(
             (parent.namespaceURI: any),
             parent.tagName,
-          );
+          )
+        : parent.ownerDocument.createElement(parent.tagName);
     testElement.innerHTML = html;
     return testElement.innerHTML;
   }
@@ -2469,7 +2474,7 @@ function diffHydratedCustomComponent(
   domElement: Element,
   tag: string,
   props: Object,
-  parentNamespaceDev: string,
+  hostContext: HostContext,
   extraAttributes: Set<string>,
 ) {
   for (const propKey in props) {
@@ -2545,11 +2550,14 @@ function diffHydratedCustomComponent(
         }
       // Fall through
       default: {
-        let ownNamespaceDev = parentNamespaceDev;
-        if (ownNamespaceDev === HTML_NAMESPACE) {
-          ownNamespaceDev = getIntrinsicNamespace(tag);
-        }
-        if (ownNamespaceDev === HTML_NAMESPACE) {
+        // This is a DEV-only path
+        const hostContextDev: HostContextDev = (hostContext: any);
+        const hostContextProd = hostContextDev.context;
+        if (
+          hostContextProd === HostContextNamespaceNone &&
+          tag !== 'svg' &&
+          tag !== 'math'
+        ) {
           extraAttributes.delete(propKey.toLowerCase());
         } else {
           extraAttributes.delete(propKey);
@@ -2576,7 +2584,7 @@ function diffHydratedGenericElement(
   domElement: Element,
   tag: string,
   props: Object,
-  parentNamespaceDev: string,
+  hostContext: HostContext,
   extraAttributes: Set<string>,
 ) {
   for (const propKey in props) {
@@ -2945,11 +2953,16 @@ function diffHydratedGenericElement(
         }
         const attributeName = getAttributeAlias(propKey);
         let isMismatchDueToBadCasing = false;
-        let ownNamespaceDev = parentNamespaceDev;
-        if (ownNamespaceDev === HTML_NAMESPACE) {
-          ownNamespaceDev = getIntrinsicNamespace(tag);
-        }
-        if (ownNamespaceDev === HTML_NAMESPACE) {
+
+        // This is a DEV-only path
+        const hostContextDev: HostContextDev = (hostContext: any);
+        const hostContextProd = hostContextDev.context;
+
+        if (
+          hostContextProd === HostContextNamespaceNone &&
+          tag !== 'svg' &&
+          tag !== 'math'
+        ) {
           extraAttributes.delete(attributeName.toLowerCase());
         } else {
           const standardName = getPossibleStandardName(propKey);
@@ -2983,7 +2996,7 @@ export function diffHydratedProperties(
   props: Object,
   isConcurrentMode: boolean,
   shouldWarnDev: boolean,
-  parentNamespaceDev: string,
+  hostContext: HostContext,
 ): null | Array<mixed> {
   if (__DEV__) {
     validatePropertiesInDevelopment(tag, props);
@@ -3155,15 +3168,15 @@ export function diffHydratedProperties(
         domElement,
         tag,
         props,
-        parentNamespaceDev,
+        hostContext,
         extraAttributes,
       );
     } else {
       diffHydratedGenericElement(
         domElement,
         tag,
         props,
-        parentNamespaceDev,
+        hostContext,
         extraAttributes,
       );
     }

filename: packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
patch: @@ -67,11 +67,7 @@ import {
   setEnabled as ReactBrowserEventEmitterSetEnabled,
   getEventPriority,
 } from '../events/ReactDOMEventListener';
-import {
-  getChildNamespace,
-  SVG_NAMESPACE,
-  MATH_NAMESPACE,
-} from './DOMNamespaces';
+import {SVG_NAMESPACE, MATH_NAMESPACE} from './DOMNamespaces';
 import {
   ELEMENT_NODE,
   TEXT_NODE,
@@ -154,11 +150,11 @@ export interface SuspenseInstance extends Comment {
 }
 export type HydratableInstance = Instance | TextInstance | SuspenseInstance;
 export type PublicInstance = Element | Text;
-type HostContextDev = {
-  namespace: HostContextProd,
+export type HostContextDev = {
+  context: HostContextProd,
   ancestorInfo: AncestorInfoDev,
 };
-type HostContextProd = string;
+type HostContextProd = HostContextNamespace;
 export type HostContext = HostContextDev | HostContextProd;
 export type UpdatePayload = Array<mixed>;
 export type ChildSet = void; // Unused
@@ -180,6 +176,11 @@ const SUSPENSE_FALLBACK_START_DATA = '$!';
 
 const STYLE = 'style';
 
+opaque type HostContextNamespace = 0 | 1 | 2;
+export const HostContextNamespaceNone: HostContextNamespace = 0;
+const HostContextNamespaceSvg: HostContextNamespace = 1;
+const HostContextNamespaceMath: HostContextNamespace = 2;
+
 let eventsEnabled: ?boolean = null;
 let selectionInformation: null | SelectionInformation = null;
 
@@ -197,33 +198,89 @@ export function getRootHostContext(
   rootContainerInstance: Container,
 ): HostContext {
   let type;
-  let namespace: HostContextProd;
+  let context: HostContextProd;
   const nodeType = rootContainerInstance.nodeType;
   switch (nodeType) {
     case DOCUMENT_NODE:
     case DOCUMENT_FRAGMENT_NODE: {
       type = nodeType === DOCUMENT_NODE ? '#document' : '#fragment';
       const root = (rootContainerInstance: any).documentElement;
-      namespace = root ? root.namespaceURI : getChildNamespace(null, '');
+      if (root) {
+        const namespaceURI = root.namespaceURI;
+        context = namespaceURI
+          ? getOwnHostContext(namespaceURI)
+          : HostContextNamespaceNone;
+      } else {
+        context = HostContextNamespaceNone;
+      }
       break;
     }
     default: {
       const container: any =
         nodeType === COMMENT_NODE
           ? rootContainerInstance.parentNode
           : rootContainerInstance;
-      const ownNamespace = container.namespaceURI || null;
       type = container.tagName;
-      namespace = getChildNamespace(ownNamespace, type);
+      const namespaceURI = container.namespaceURI;
+      if (!namespaceURI) {
+        switch (type) {
+          case 'svg':
+            context = HostContextNamespaceSvg;
+            break;
+          case 'math':
+            context = HostContextNamespaceMath;
+            break;
+          default:
+            context = HostContextNamespaceNone;
+            break;
+        }
+      } else {
+        const ownContext = getOwnHostContext(namespaceURI);
+        context = getChildHostContextProd(ownContext, type);
+      }
       break;
     }
   }
   if (__DEV__) {
     const validatedTag = type.toLowerCase();
     const ancestorInfo = updatedAncestorInfoDev(null, validatedTag);
-    return {namespace, ancestorInfo};
+    return {context, ancestorInfo};
+  }
+  return context;
+}
+
+function getOwnHostContext(namespaceURI: string): HostContextNamespace {
+  switch (namespaceURI) {
+    case SVG_NAMESPACE:
+      return HostContextNamespaceSvg;
+    case MATH_NAMESPACE:
+      return HostContextNamespaceMath;
+    default:
+      return HostContextNamespaceNone;
+  }
+}
+
+function getChildHostContextProd(
+  parentNamespace: HostContextNamespace,
+  type: string,
+): HostContextNamespace {
+  if (parentNamespace === HostContextNamespaceNone) {
+    // No (or default) parent namespace: potential entry point.
+    switch (type) {
+      case 'svg':
+        return HostContextNamespaceSvg;
+      case 'math':
+        return HostContextNamespaceMath;
+      default:
+        return HostContextNamespaceNone;
+    }
+  }
+  if (parentNamespace === HostContextNamespaceSvg && type === 'foreignObject') {
+    // We're leaving SVG.
+    return HostContextNamespaceNone;
   }
-  return namespace;
+  // By default, pass namespace below.
+  return parentNamespace;
 }
 
 export function getChildHostContext(
@@ -232,15 +289,15 @@ export function getChildHostContext(
 ): HostContext {
   if (__DEV__) {
     const parentHostContextDev = ((parentHostContext: any): HostContextDev);
-    const namespace = getChildNamespace(parentHostContextDev.namespace, type);
+    const context = getChildHostContextProd(parentHostContextDev.context, type);
     const ancestorInfo = updatedAncestorInfoDev(
       parentHostContextDev.ancestorInfo,
       type,
     );
-    return {namespace, ancestorInfo};
+    return {context, ancestorInfo};
   }
   const parentNamespace = ((parentHostContext: any): HostContextProd);
-  return getChildNamespace(parentNamespace, type);
+  return getChildHostContextProd(parentNamespace, type);
 }
 
 export function getPublicInstance(instance: Instance): Instance {
@@ -326,26 +383,27 @@ export function createInstance(
   hostContext: HostContext,
   internalInstanceHandle: Object,
 ): Instance {
-  let namespace;
+  let hostContextProd: HostContextProd;
   if (__DEV__) {
     // TODO: take namespace into account when validating.
     const hostContextDev: HostContextDev = (hostContext: any);
     validateDOMNesting(type, hostContextDev.ancestorInfo);
-    namespace = hostContextDev.namespace;
+    hostContextProd = hostContextDev.context;
   } else {
-    const hostContextProd: HostContextProd = (hostContext: any);
-    namespace = hostContextProd;
+    hostContextProd = (hostContext: any);
   }
 
   const ownerDocument = getOwnerDocumentFromRootContainer(
     rootContainerInstance,
   );
 
   let domElement: Instance;
-  switch (namespace) {
-    case SVG_NAMESPACE:
-    case MATH_NAMESPACE:
-      domElement = ownerDocument.createElementNS(namespace, type);
+  switch (hostContextProd) {
+    case HostContextNamespaceSvg:
+      domElement = ownerDocument.createElementNS(SVG_NAMESPACE, type);
+      break;
+    case HostContextNamespaceMath:
+      domElement = ownerDocument.createElementNS(MATH_NAMESPACE, type);
       break;
     default:
       switch (type) {
@@ -1243,22 +1301,13 @@ export function hydrateInstance(
   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,
     isConcurrentMode,
     shouldWarnDev,
-    parentNamespace,
+    hostContext,
   );
 }
 
@@ -2924,20 +2973,19 @@ export function isHostHoistableType(
   hostContext: HostContext,
 ): boolean {
   let outsideHostContainerContext: boolean;
-  let namespace: HostContextProd;
+  let hostContextProd: HostContextProd;
   if (__DEV__) {
     const hostContextDev: HostContextDev = (hostContext: any);
     // We can only render resources when we are not within the host container context
     outsideHostContainerContext =
       !hostContextDev.ancestorInfo.containerTagInScope;
-    namespace = hostContextDev.namespace;
+    hostContextProd = hostContextDev.context;
   } else {
-    const hostContextProd: HostContextProd = (hostContext: any);
-    namespace = hostContextProd;
+    hostContextProd = (hostContext: any);
   }
 
   // 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 (hostContextProd === HostContextNamespaceSvg || props.itemProp != null) {
     if (__DEV__) {
       if (
         outsideHostContainerContext &&