From 7022eece8b2ba00452016764cdb59cf37f633e68 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 4 Apr 2023 11:45:32 -0400 Subject: [PATCH 1/5] Move simple aliases to Map/Set instead of switch statements for performance Unfortunately VMs don't optimize this well enough yet. Most special cases are still in the switch but now there are few enough cases that it's not bad. --- .../src/client/ReactDOMComponent.js | 1051 +---------------- .../src/server/ReactDOMServerFormatConfig.js | 253 +--- .../src/shared/getAttributeAlias.js | 98 ++ .../src/shared/isUnitlessNumber.js | 150 ++- 4 files changed, 233 insertions(+), 1319 deletions(-) create mode 100644 packages/react-dom-bindings/src/shared/getAttributeAlias.js diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index 60776996e537a..b8bb564a330d7 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -58,6 +58,7 @@ import { } from './CSSPropertyOperations'; import {HTML_NAMESPACE, getIntrinsicNamespace} from './DOMNamespaces'; import isCustomElement from '../shared/isCustomElement'; +import getAttributeAlias from '../shared/getAttributeAlias'; import possibleStandardNames from '../shared/possibleStandardNames'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; import {validateProperties as validateInputProperties} from '../shared/ReactDOMNullInputValuePropHook'; @@ -565,251 +566,6 @@ function setProp( } break; } - // A few React string attributes have a different name. - // This is a mapping from React prop names to the attribute names. - case 'acceptCharset': - setValueForAttribute(domElement, 'accept-charset', value); - break; - case 'className': - setValueForAttribute(domElement, 'class', value); - break; - case 'htmlFor': - setValueForAttribute(domElement, 'for', value); - break; - case 'httpEquiv': - setValueForAttribute(domElement, 'http-equiv', value); - break; - // HTML and SVG attributes, but the SVG attribute is case sensitive. - case 'tabIndex': - setValueForAttribute(domElement, 'tabindex', value); - break; - case 'crossOrigin': - setValueForAttribute(domElement, 'crossorigin', value); - break; - // This is a list of all SVG attributes that need special casing. - // Regular attributes that just accept strings. - case 'accentHeight': - setValueForAttribute(domElement, 'accent-height', value); - break; - case 'alignmentBaseline': - setValueForAttribute(domElement, 'alignment-baseline', value); - break; - case 'arabicForm': - setValueForAttribute(domElement, 'arabic-form', value); - break; - case 'baselineShift': - setValueForAttribute(domElement, 'baseline-shift', value); - break; - case 'capHeight': - setValueForAttribute(domElement, 'cap-height', value); - break; - case 'clipPath': - setValueForAttribute(domElement, 'clip-path', value); - break; - case 'clipRule': - setValueForAttribute(domElement, 'clip-rule', value); - break; - case 'colorInterpolation': - setValueForAttribute(domElement, 'color-interpolation', value); - break; - case 'colorInterpolationFilters': - setValueForAttribute(domElement, 'color-interpolation-filters', value); - break; - case 'colorProfile': - setValueForAttribute(domElement, 'color-profile', value); - break; - case 'colorRendering': - setValueForAttribute(domElement, 'color-rendering', value); - break; - case 'dominantBaseline': - setValueForAttribute(domElement, 'dominant-baseline', value); - break; - case 'enableBackground': - setValueForAttribute(domElement, 'enable-background', value); - break; - case 'fillOpacity': - setValueForAttribute(domElement, 'fill-opacity', value); - break; - case 'fillRule': - setValueForAttribute(domElement, 'fill-rule', value); - break; - case 'floodColor': - setValueForAttribute(domElement, 'flood-color', value); - break; - case 'floodOpacity': - setValueForAttribute(domElement, 'flood-opacity', value); - break; - case 'fontFamily': - setValueForAttribute(domElement, 'font-family', value); - break; - case 'fontSize': - setValueForAttribute(domElement, 'font-size', value); - break; - case 'fontSizeAdjust': - setValueForAttribute(domElement, 'font-size-adjust', value); - break; - case 'fontStretch': - setValueForAttribute(domElement, 'font-stretch', value); - break; - case 'fontStyle': - setValueForAttribute(domElement, 'font-style', value); - break; - case 'fontVariant': - setValueForAttribute(domElement, 'font-variant', value); - break; - case 'fontWeight': - setValueForAttribute(domElement, 'font-weight', value); - break; - case 'glyphName': - setValueForAttribute(domElement, 'glyph-name', value); - break; - case 'glyphOrientationHorizontal': - setValueForAttribute(domElement, 'glyph-orientation-horizontal', value); - break; - case 'glyphOrientationVertical': - setValueForAttribute(domElement, 'glyph-orientation-vertical', value); - break; - case 'horizAdvX': - setValueForAttribute(domElement, 'horiz-adv-x', value); - break; - case 'horizOriginX': - setValueForAttribute(domElement, 'horiz-origin-x', value); - break; - case 'imageRendering': - setValueForAttribute(domElement, 'image-rendering', value); - break; - case 'letterSpacing': - setValueForAttribute(domElement, 'letter-spacing', value); - break; - case 'lightingColor': - setValueForAttribute(domElement, 'lighting-color', value); - break; - case 'markerEnd': - setValueForAttribute(domElement, 'marker-end', value); - break; - case 'markerMid': - setValueForAttribute(domElement, 'marker-mid', value); - break; - case 'markerStart': - setValueForAttribute(domElement, 'marker-start', value); - break; - case 'overlinePosition': - setValueForAttribute(domElement, 'overline-position', value); - break; - case 'overlineThickness': - setValueForAttribute(domElement, 'overline-thickness', value); - break; - case 'paintOrder': - setValueForAttribute(domElement, 'paint-order', value); - break; - case 'panose-1': - setValueForAttribute(domElement, 'panose-1', value); - break; - case 'pointerEvents': - setValueForAttribute(domElement, 'pointer-events', value); - break; - case 'renderingIntent': - setValueForAttribute(domElement, 'rendering-intent', value); - break; - case 'shapeRendering': - setValueForAttribute(domElement, 'shape-rendering', value); - break; - case 'stopColor': - setValueForAttribute(domElement, 'stop-color', value); - break; - case 'stopOpacity': - setValueForAttribute(domElement, 'stop-opacity', value); - break; - case 'strikethroughPosition': - setValueForAttribute(domElement, 'strikethrough-position', value); - break; - case 'strikethroughThickness': - setValueForAttribute(domElement, 'strikethrough-thickness', value); - break; - case 'strokeDasharray': - setValueForAttribute(domElement, 'stroke-dasharray', value); - break; - case 'strokeDashoffset': - setValueForAttribute(domElement, 'stroke-dashoffset', value); - break; - case 'strokeLinecap': - setValueForAttribute(domElement, 'stroke-linecap', value); - break; - case 'strokeLinejoin': - setValueForAttribute(domElement, 'stroke-linejoin', value); - break; - case 'strokeMiterlimit': - setValueForAttribute(domElement, 'stroke-miterlimit', value); - break; - case 'strokeOpacity': - setValueForAttribute(domElement, 'stroke-opacity', value); - break; - case 'strokeWidth': - setValueForAttribute(domElement, 'stroke-width', value); - break; - case 'textAnchor': - setValueForAttribute(domElement, 'text-anchor', value); - break; - case 'textDecoration': - setValueForAttribute(domElement, 'text-decoration', value); - break; - case 'textRendering': - setValueForAttribute(domElement, 'text-rendering', value); - break; - case 'transformOrigin': - setValueForAttribute(domElement, 'transform-origin', value); - break; - case 'underlinePosition': - setValueForAttribute(domElement, 'underline-position', value); - break; - case 'underlineThickness': - setValueForAttribute(domElement, 'underline-thickness', value); - break; - case 'unicodeBidi': - setValueForAttribute(domElement, 'unicode-bidi', value); - break; - case 'unicodeRange': - setValueForAttribute(domElement, 'unicode-range', value); - break; - case 'unitsPerEm': - setValueForAttribute(domElement, 'units-per-em', value); - break; - case 'vAlphabetic': - setValueForAttribute(domElement, 'v-alphabetic', value); - break; - case 'vHanging': - setValueForAttribute(domElement, 'v-hanging', value); - break; - case 'vIdeographic': - setValueForAttribute(domElement, 'v-ideographic', value); - break; - case 'vMathematical': - setValueForAttribute(domElement, 'v-mathematical', value); - break; - case 'vectorEffect': - setValueForAttribute(domElement, 'vector-effect', value); - break; - case 'vertAdvY': - setValueForAttribute(domElement, 'vert-adv-y', value); - break; - case 'vertOriginX': - setValueForAttribute(domElement, 'vert-origin-x', value); - break; - case 'vertOriginY': - setValueForAttribute(domElement, 'vert-origin-y', value); - break; - case 'wordSpacing': - setValueForAttribute(domElement, 'word-spacing', value); - break; - case 'writingMode': - setValueForAttribute(domElement, 'writing-mode', value); - break; - case 'xmlnsXlink': - setValueForAttribute(domElement, 'xmlns:xlink', value); - break; - case 'xHeight': - setValueForAttribute(domElement, 'x-height', value); - break; case 'xlinkActuate': setValueForNamespacedAttribute( domElement, @@ -904,7 +660,8 @@ function setProp( warnForInvalidEventListener(key, value); } } else { - setValueForAttribute(domElement, key, value); + const attributeName = getAttributeAlias(key); + setValueForAttribute(domElement, attributeName, value); } } } @@ -2244,828 +2001,132 @@ function diffHydratedGenericElement( ); continue; } - // A few React string attributes have a different name. - // This is a mapping from React prop names to the attribute names. - case 'acceptCharset': - hydrateAttribute( - domElement, - propKey, - 'accept-charset', - value, - extraAttributes, - ); - continue; - case 'className': - hydrateAttribute(domElement, propKey, 'class', value, extraAttributes); - continue; - case 'htmlFor': - hydrateAttribute(domElement, propKey, 'for', value, extraAttributes); - continue; - case 'httpEquiv': - hydrateAttribute( - domElement, - propKey, - 'http-equiv', - value, - extraAttributes, - ); - continue; - case 'tabIndex': - hydrateAttribute( - domElement, - propKey, - 'tabindex', - value, - extraAttributes, - ); - continue; - case 'crossOrigin': - hydrateAttribute( - domElement, - propKey, - 'crossorigin', - value, - extraAttributes, - ); - continue; - case 'accentHeight': - hydrateAttribute( - domElement, - propKey, - 'accent-height', - value, - extraAttributes, - ); - continue; - case 'alignmentBaseline': - hydrateAttribute( - domElement, - propKey, - 'alignment-baseline', - value, - extraAttributes, - ); - continue; - case 'arabicForm': - hydrateAttribute( - domElement, - propKey, - 'arabic-form', - value, - extraAttributes, - ); - continue; - case 'baselineShift': - hydrateAttribute( - domElement, - propKey, - 'baseline-shift', - value, - extraAttributes, - ); - continue; - case 'capHeight': - hydrateAttribute( - domElement, - propKey, - 'cap-height', - value, - extraAttributes, - ); - continue; - case 'clipPath': - hydrateAttribute( - domElement, - propKey, - 'clip-path', - value, - extraAttributes, - ); - continue; - case 'clipRule': - hydrateAttribute( - domElement, - propKey, - 'clip-rule', - value, - extraAttributes, - ); - continue; - case 'colorInterpolation': - hydrateAttribute( - domElement, - propKey, - 'color-interpolation', - value, - extraAttributes, - ); - continue; - case 'colorInterpolationFilters': - hydrateAttribute( - domElement, - propKey, - 'color-interpolation-filters', - value, - extraAttributes, - ); - continue; - case 'colorProfile': - hydrateAttribute( - domElement, - propKey, - 'color-profile', - value, - extraAttributes, - ); - continue; - case 'colorRendering': - hydrateAttribute( - domElement, - propKey, - 'color-rendering', - value, - extraAttributes, - ); - continue; - case 'dominantBaseline': - hydrateAttribute( - domElement, - propKey, - 'dominant-baseline', - value, - extraAttributes, - ); - continue; - case 'enableBackground': - hydrateAttribute( - domElement, - propKey, - 'enable-background', - value, - extraAttributes, - ); - continue; - case 'fillOpacity': + case 'xHeight': hydrateAttribute( domElement, propKey, - 'fill-opacity', + 'x-height', value, extraAttributes, ); continue; - case 'fillRule': + case 'xlinkActuate': hydrateAttribute( domElement, propKey, - 'fill-rule', + 'xlink:actuate', value, extraAttributes, ); continue; - case 'floodColor': + case 'xlinkArcrole': hydrateAttribute( domElement, propKey, - 'flood-color', + 'xlink:arcrole', value, extraAttributes, ); continue; - case 'floodOpacity': + case 'xlinkRole': hydrateAttribute( domElement, propKey, - 'flood-opacity', + 'xlink:role', value, extraAttributes, ); continue; - case 'fontFamily': + case 'xlinkShow': hydrateAttribute( domElement, propKey, - 'font-family', + 'xlink:show', value, extraAttributes, ); continue; - case 'fontSize': + case 'xlinkTitle': hydrateAttribute( domElement, propKey, - 'font-size', + 'xlink:title', value, extraAttributes, ); continue; - case 'fontSizeAdjust': + case 'xlinkType': hydrateAttribute( domElement, propKey, - 'font-size-adjust', + 'xlink:type', value, extraAttributes, ); continue; - case 'fontStretch': + case 'xmlBase': hydrateAttribute( domElement, propKey, - 'font-stretch', + 'xml:base', value, extraAttributes, ); continue; - case 'fontStyle': + case 'xmlLang': hydrateAttribute( domElement, propKey, - 'font-style', + 'xml:lang', value, extraAttributes, ); continue; - case 'fontVariant': + case 'xmlSpace': hydrateAttribute( domElement, propKey, - 'font-variant', + 'xml:space', value, extraAttributes, ); continue; - case 'fontWeight': - hydrateAttribute( + default: { + if ( + // shouldIgnoreAttribute + // We have already filtered out null/undefined and reserved words. + propKey.length > 2 && + (propKey[0] === 'o' || propKey[0] === 'O') && + (propKey[1] === 'n' || propKey[1] === 'N') + ) { + continue; + } + const attributeName = getAttributeAlias(propKey); + let isMismatchDueToBadCasing = false; + let ownNamespaceDev = parentNamespaceDev; + if (ownNamespaceDev === HTML_NAMESPACE) { + ownNamespaceDev = getIntrinsicNamespace(tag); + } + if (ownNamespaceDev === HTML_NAMESPACE) { + extraAttributes.delete(attributeName.toLowerCase()); + } else { + const standardName = getPossibleStandardName(propKey); + if (standardName !== null && standardName !== propKey) { + // If an SVG prop is supplied with bad casing, it will + // be successfully parsed from HTML, but will produce a mismatch + // (and would be incorrectly rendered on the client). + // However, we already warn about bad casing elsewhere. + // So we'll skip the misleading extra mismatch warning in this case. + isMismatchDueToBadCasing = true; + extraAttributes.delete(standardName); + } + extraAttributes.delete(attributeName); + } + const serverValue = getValueForAttribute( domElement, - propKey, - 'font-weight', + attributeName, value, - extraAttributes, ); - continue; - case 'glyphName': - hydrateAttribute( - domElement, - propKey, - 'glyph-name', - value, - extraAttributes, - ); - continue; - case 'glyphOrientationHorizontal': - hydrateAttribute( - domElement, - propKey, - 'glyph-orientation-horizontal', - value, - extraAttributes, - ); - continue; - case 'glyphOrientationVertical': - hydrateAttribute( - domElement, - propKey, - 'glyph-orientation-vertical', - value, - extraAttributes, - ); - continue; - case 'horizAdvX': - hydrateAttribute( - domElement, - propKey, - 'horiz-adv-x', - value, - extraAttributes, - ); - continue; - case 'horizOriginX': - hydrateAttribute( - domElement, - propKey, - 'horiz-origin-x', - value, - extraAttributes, - ); - continue; - case 'imageRendering': - hydrateAttribute( - domElement, - propKey, - 'image-rendering', - value, - extraAttributes, - ); - continue; - case 'letterSpacing': - hydrateAttribute( - domElement, - propKey, - 'letter-spacing', - value, - extraAttributes, - ); - continue; - case 'lightingColor': - hydrateAttribute( - domElement, - propKey, - 'lighting-color', - value, - extraAttributes, - ); - continue; - case 'markerEnd': - hydrateAttribute( - domElement, - propKey, - 'marker-end', - value, - extraAttributes, - ); - continue; - case 'markerMid': - hydrateAttribute( - domElement, - propKey, - 'marker-mid', - value, - extraAttributes, - ); - continue; - case 'markerStart': - hydrateAttribute( - domElement, - propKey, - 'marker-start', - value, - extraAttributes, - ); - continue; - case 'overlinePosition': - hydrateAttribute( - domElement, - propKey, - 'overline-position', - value, - extraAttributes, - ); - continue; - case 'overlineThickness': - hydrateAttribute( - domElement, - propKey, - 'overline-thickness', - value, - extraAttributes, - ); - continue; - case 'paintOrder': - hydrateAttribute( - domElement, - propKey, - 'paint-order', - value, - extraAttributes, - ); - continue; - case 'panose-1': - hydrateAttribute( - domElement, - propKey, - 'panose-1', - value, - extraAttributes, - ); - continue; - case 'pointerEvents': - hydrateAttribute( - domElement, - propKey, - 'pointer-events', - value, - extraAttributes, - ); - continue; - case 'renderingIntent': - hydrateAttribute( - domElement, - propKey, - 'rendering-intent', - value, - extraAttributes, - ); - continue; - case 'shapeRendering': - hydrateAttribute( - domElement, - propKey, - 'shape-rendering', - value, - extraAttributes, - ); - continue; - case 'stopColor': - hydrateAttribute( - domElement, - propKey, - 'stop-color', - value, - extraAttributes, - ); - continue; - case 'stopOpacity': - hydrateAttribute( - domElement, - propKey, - 'stop-opacity', - value, - extraAttributes, - ); - continue; - case 'strikethroughPosition': - hydrateAttribute( - domElement, - propKey, - 'strikethrough-position', - value, - extraAttributes, - ); - continue; - case 'strikethroughThickness': - hydrateAttribute( - domElement, - propKey, - 'strikethrough-thickness', - value, - extraAttributes, - ); - continue; - case 'strokeDasharray': - hydrateAttribute( - domElement, - propKey, - 'stroke-dasharray', - value, - extraAttributes, - ); - continue; - case 'strokeDashoffset': - hydrateAttribute( - domElement, - propKey, - 'stroke-dashoffset', - value, - extraAttributes, - ); - continue; - case 'strokeLinecap': - hydrateAttribute( - domElement, - propKey, - 'stroke-linecap', - value, - extraAttributes, - ); - continue; - case 'strokeLinejoin': - hydrateAttribute( - domElement, - propKey, - 'stroke-linejoin', - value, - extraAttributes, - ); - continue; - case 'strokeMiterlimit': - hydrateAttribute( - domElement, - propKey, - 'stroke-miterlimit', - value, - extraAttributes, - ); - continue; - case 'strokeOpacity': - hydrateAttribute( - domElement, - propKey, - 'stroke-opacity', - value, - extraAttributes, - ); - continue; - case 'strokeWidth': - hydrateAttribute( - domElement, - propKey, - 'stroke-width', - value, - extraAttributes, - ); - continue; - case 'textAnchor': - hydrateAttribute( - domElement, - propKey, - 'text-anchor', - value, - extraAttributes, - ); - continue; - case 'textDecoration': - hydrateAttribute( - domElement, - propKey, - 'text-decoration', - value, - extraAttributes, - ); - continue; - case 'textRendering': - hydrateAttribute( - domElement, - propKey, - 'text-rendering', - value, - extraAttributes, - ); - continue; - case 'transformOrigin': - hydrateAttribute( - domElement, - propKey, - 'transform-origin', - value, - extraAttributes, - ); - continue; - case 'underlinePosition': - hydrateAttribute( - domElement, - propKey, - 'underline-position', - value, - extraAttributes, - ); - continue; - case 'underlineThickness': - hydrateAttribute( - domElement, - propKey, - 'underline-thickness', - value, - extraAttributes, - ); - continue; - case 'unicodeBidi': - hydrateAttribute( - domElement, - propKey, - 'unicode-bidi', - value, - extraAttributes, - ); - continue; - case 'unicodeRange': - hydrateAttribute( - domElement, - propKey, - 'unicode-range', - value, - extraAttributes, - ); - continue; - case 'unitsPerEm': - hydrateAttribute( - domElement, - propKey, - 'units-per-em', - value, - extraAttributes, - ); - continue; - case 'vAlphabetic': - hydrateAttribute( - domElement, - propKey, - 'v-alphabetic', - value, - extraAttributes, - ); - continue; - case 'vHanging': - hydrateAttribute( - domElement, - propKey, - 'v-hanging', - value, - extraAttributes, - ); - continue; - case 'vIdeographic': - hydrateAttribute( - domElement, - propKey, - 'v-ideographic', - value, - extraAttributes, - ); - continue; - case 'vMathematical': - hydrateAttribute( - domElement, - propKey, - 'v-mathematical', - value, - extraAttributes, - ); - continue; - case 'vectorEffect': - hydrateAttribute( - domElement, - propKey, - 'vector-effect', - value, - extraAttributes, - ); - continue; - case 'vertAdvY': - hydrateAttribute( - domElement, - propKey, - 'vert-adv-y', - value, - extraAttributes, - ); - continue; - case 'vertOriginX': - hydrateAttribute( - domElement, - propKey, - 'vert-origin-x', - value, - extraAttributes, - ); - continue; - case 'vertOriginY': - hydrateAttribute( - domElement, - propKey, - 'vert-origin-y', - value, - extraAttributes, - ); - continue; - case 'wordSpacing': - hydrateAttribute( - domElement, - propKey, - 'word-spacing', - value, - extraAttributes, - ); - continue; - case 'writingMode': - hydrateAttribute( - domElement, - propKey, - 'writing-mode', - value, - extraAttributes, - ); - continue; - case 'xmlnsXlink': - hydrateAttribute( - domElement, - propKey, - 'xmlns:xlink', - value, - extraAttributes, - ); - continue; - case 'xHeight': - hydrateAttribute( - domElement, - propKey, - 'x-height', - value, - extraAttributes, - ); - continue; - case 'xlinkActuate': - hydrateAttribute( - domElement, - propKey, - 'xlink:actuate', - value, - extraAttributes, - ); - continue; - case 'xlinkArcrole': - hydrateAttribute( - domElement, - propKey, - 'xlink:arcrole', - value, - extraAttributes, - ); - continue; - case 'xlinkRole': - hydrateAttribute( - domElement, - propKey, - 'xlink:role', - value, - extraAttributes, - ); - continue; - case 'xlinkShow': - hydrateAttribute( - domElement, - propKey, - 'xlink:show', - value, - extraAttributes, - ); - continue; - case 'xlinkTitle': - hydrateAttribute( - domElement, - propKey, - 'xlink:title', - value, - extraAttributes, - ); - continue; - case 'xlinkType': - hydrateAttribute( - domElement, - propKey, - 'xlink:type', - value, - extraAttributes, - ); - continue; - case 'xmlBase': - hydrateAttribute( - domElement, - propKey, - 'xml:base', - value, - extraAttributes, - ); - continue; - case 'xmlLang': - hydrateAttribute( - domElement, - propKey, - 'xml:lang', - value, - extraAttributes, - ); - continue; - case 'xmlSpace': - hydrateAttribute( - domElement, - propKey, - 'xml:space', - value, - extraAttributes, - ); - continue; - default: { - if ( - // shouldIgnoreAttribute - // We have already filtered out null/undefined and reserved words. - propKey.length > 2 && - (propKey[0] === 'o' || propKey[0] === 'O') && - (propKey[1] === 'n' || propKey[1] === 'N') - ) { - continue; - } - let isMismatchDueToBadCasing = false; - let ownNamespaceDev = parentNamespaceDev; - if (ownNamespaceDev === HTML_NAMESPACE) { - ownNamespaceDev = getIntrinsicNamespace(tag); - } - if (ownNamespaceDev === HTML_NAMESPACE) { - extraAttributes.delete(propKey.toLowerCase()); - } else { - const standardName = getPossibleStandardName(propKey); - if (standardName !== null && standardName !== propKey) { - // If an SVG prop is supplied with bad casing, it will - // be successfully parsed from HTML, but will produce a mismatch - // (and would be incorrectly rendered on the client). - // However, we already warn about bad casing elsewhere. - // So we'll skip the misleading extra mismatch warning in this case. - isMismatchDueToBadCasing = true; - extraAttributes.delete(standardName); - } - extraAttributes.delete(propKey); - } - const serverValue = getValueForAttribute(domElement, propKey, value); if (!isMismatchDueToBadCasing) { warnForPropDifference(propKey, serverValue, value); } diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 601706df50157..3b8e876268f8f 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -40,6 +40,7 @@ import { import isAttributeNameSafe from '../shared/isAttributeNameSafe'; import isUnitlessNumber from '../shared/isUnitlessNumber'; +import getAttributeAlias from '../shared/getAttributeAlias'; import {checkControlledValueProps} from '../shared/ReactControlledValuePropTypes'; import {validateProperties as validateARIAProperties} from '../shared/ReactDOMInvalidARIAHook'; @@ -846,251 +847,6 @@ function pushAttribute( } return; } - // A few React string attributes have a different name. - // This is a mapping from React prop names to the attribute names. - case 'acceptCharset': - pushStringAttribute(target, 'accept-charset', value); - return; - case 'className': - pushStringAttribute(target, 'class', value); - return; - case 'htmlFor': - pushStringAttribute(target, 'for', value); - return; - case 'httpEquiv': - pushStringAttribute(target, 'http-equiv', value); - return; - // HTML and SVG attributes, but the SVG attribute is case sensitive. - case 'tabIndex': - pushStringAttribute(target, 'tabindex', value); - return; - case 'crossOrigin': - pushStringAttribute(target, 'crossorigin', value); - return; - // This is a list of all SVG attributes that need special casing. - // Regular attributes that just accept strings. - case 'accentHeight': - pushStringAttribute(target, 'accent-height', value); - return; - case 'alignmentBaseline': - pushStringAttribute(target, 'alignment-baseline', value); - return; - case 'arabicForm': - pushStringAttribute(target, 'arabic-form', value); - return; - case 'baselineShift': - pushStringAttribute(target, 'baseline-shift', value); - return; - case 'capHeight': - pushStringAttribute(target, 'cap-height', value); - return; - case 'clipPath': - pushStringAttribute(target, 'clip-path', value); - return; - case 'clipRule': - pushStringAttribute(target, 'clip-rule', value); - return; - case 'colorInterpolation': - pushStringAttribute(target, 'color-interpolation', value); - return; - case 'colorInterpolationFilters': - pushStringAttribute(target, 'color-interpolation-filters', value); - return; - case 'colorProfile': - pushStringAttribute(target, 'color-profile', value); - return; - case 'colorRendering': - pushStringAttribute(target, 'color-rendering', value); - return; - case 'dominantBaseline': - pushStringAttribute(target, 'dominant-baseline', value); - return; - case 'enableBackground': - pushStringAttribute(target, 'enable-background', value); - return; - case 'fillOpacity': - pushStringAttribute(target, 'fill-opacity', value); - return; - case 'fillRule': - pushStringAttribute(target, 'fill-rule', value); - return; - case 'floodColor': - pushStringAttribute(target, 'flood-color', value); - return; - case 'floodOpacity': - pushStringAttribute(target, 'flood-opacity', value); - return; - case 'fontFamily': - pushStringAttribute(target, 'font-family', value); - return; - case 'fontSize': - pushStringAttribute(target, 'font-size', value); - return; - case 'fontSizeAdjust': - pushStringAttribute(target, 'font-size-adjust', value); - return; - case 'fontStretch': - pushStringAttribute(target, 'font-stretch', value); - return; - case 'fontStyle': - pushStringAttribute(target, 'font-style', value); - return; - case 'fontVariant': - pushStringAttribute(target, 'font-variant', value); - return; - case 'fontWeight': - pushStringAttribute(target, 'font-weight', value); - return; - case 'glyphName': - pushStringAttribute(target, 'glyph-name', value); - return; - case 'glyphOrientationHorizontal': - pushStringAttribute(target, 'glyph-orientation-horizontal', value); - return; - case 'glyphOrientationVertical': - pushStringAttribute(target, 'glyph-orientation-vertical', value); - return; - case 'horizAdvX': - pushStringAttribute(target, 'horiz-adv-x', value); - return; - case 'horizOriginX': - pushStringAttribute(target, 'horiz-origin-x', value); - return; - case 'imageRendering': - pushStringAttribute(target, 'image-rendering', value); - return; - case 'letterSpacing': - pushStringAttribute(target, 'letter-spacing', value); - return; - case 'lightingColor': - pushStringAttribute(target, 'lighting-color', value); - return; - case 'markerEnd': - pushStringAttribute(target, 'marker-end', value); - return; - case 'markerMid': - pushStringAttribute(target, 'marker-mid', value); - return; - case 'markerStart': - pushStringAttribute(target, 'marker-start', value); - return; - case 'overlinePosition': - pushStringAttribute(target, 'overline-position', value); - return; - case 'overlineThickness': - pushStringAttribute(target, 'overline-thickness', value); - return; - case 'paintOrder': - pushStringAttribute(target, 'paint-order', value); - return; - case 'panose-1': - pushStringAttribute(target, 'panose-1', value); - return; - case 'pointerEvents': - pushStringAttribute(target, 'pointer-events', value); - return; - case 'renderingIntent': - pushStringAttribute(target, 'rendering-intent', value); - return; - case 'shapeRendering': - pushStringAttribute(target, 'shape-rendering', value); - return; - case 'stopColor': - pushStringAttribute(target, 'stop-color', value); - return; - case 'stopOpacity': - pushStringAttribute(target, 'stop-opacity', value); - return; - case 'strikethroughPosition': - pushStringAttribute(target, 'strikethrough-position', value); - return; - case 'strikethroughThickness': - pushStringAttribute(target, 'strikethrough-thickness', value); - return; - case 'strokeDasharray': - pushStringAttribute(target, 'stroke-dasharray', value); - return; - case 'strokeDashoffset': - pushStringAttribute(target, 'stroke-dashoffset', value); - return; - case 'strokeLinecap': - pushStringAttribute(target, 'stroke-linecap', value); - return; - case 'strokeLinejoin': - pushStringAttribute(target, 'stroke-linejoin', value); - return; - case 'strokeMiterlimit': - pushStringAttribute(target, 'stroke-miterlimit', value); - return; - case 'strokeOpacity': - pushStringAttribute(target, 'stroke-opacity', value); - return; - case 'strokeWidth': - pushStringAttribute(target, 'stroke-width', value); - return; - case 'textAnchor': - pushStringAttribute(target, 'text-anchor', value); - return; - case 'textDecoration': - pushStringAttribute(target, 'text-decoration', value); - return; - case 'textRendering': - pushStringAttribute(target, 'text-rendering', value); - return; - case 'transformOrigin': - pushStringAttribute(target, 'transform-origin', value); - return; - case 'underlinePosition': - pushStringAttribute(target, 'underline-position', value); - return; - case 'underlineThickness': - pushStringAttribute(target, 'underline-thickness', value); - return; - case 'unicodeBidi': - pushStringAttribute(target, 'unicode-bidi', value); - return; - case 'unicodeRange': - pushStringAttribute(target, 'unicode-range', value); - return; - case 'unitsPerEm': - pushStringAttribute(target, 'units-per-em', value); - return; - case 'vAlphabetic': - pushStringAttribute(target, 'v-alphabetic', value); - return; - case 'vHanging': - pushStringAttribute(target, 'v-hanging', value); - return; - case 'vIdeographic': - pushStringAttribute(target, 'v-ideographic', value); - return; - case 'vMathematical': - pushStringAttribute(target, 'v-mathematical', value); - return; - case 'vectorEffect': - pushStringAttribute(target, 'vector-effect', value); - return; - case 'vertAdvY': - pushStringAttribute(target, 'vert-adv-y', value); - return; - case 'vertOriginX': - pushStringAttribute(target, 'vert-origin-x', value); - return; - case 'vertOriginY': - pushStringAttribute(target, 'vert-origin-y', value); - return; - case 'wordSpacing': - pushStringAttribute(target, 'word-spacing', value); - return; - case 'writingMode': - pushStringAttribute(target, 'writing-mode', value); - return; - case 'xmlnsXlink': - pushStringAttribute(target, 'xmlns:xlink', value); - return; - case 'xHeight': - pushStringAttribute(target, 'x-height', value); - return; case 'xlinkActuate': pushStringAttribute(target, 'xlink:actuate', value); break; @@ -1129,14 +885,15 @@ function pushAttribute( return; } - if (isAttributeNameSafe(name)) { + const attributeName = getAttributeAlias(name); + if (isAttributeNameSafe(attributeName)) { // shouldRemoveAttribute switch (typeof value) { case 'function': case 'symbol': // eslint-disable-line return; case 'boolean': { - const prefix = name.toLowerCase().slice(0, 5); + const prefix = attributeName.toLowerCase().slice(0, 5); if (prefix !== 'data-' && prefix !== 'aria-') { return; } @@ -1144,7 +901,7 @@ function pushAttribute( } target.push( attributeSeparator, - stringToChunk(name), + stringToChunk(attributeName), attributeAssign, stringToChunk(escapeTextForBrowser(value)), attributeEnd, diff --git a/packages/react-dom-bindings/src/shared/getAttributeAlias.js b/packages/react-dom-bindings/src/shared/getAttributeAlias.js new file mode 100644 index 0000000000000..07add09914ec1 --- /dev/null +++ b/packages/react-dom-bindings/src/shared/getAttributeAlias.js @@ -0,0 +1,98 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +const aliases = new Map([ + ['acceptCharset', 'accept-charset'], + ['className', 'class'], + ['htmlFor', 'for'], + ['httpEquiv', 'http-equiv'], + // HTML and SVG attributes, but the SVG attribute is sensitive.], + ['tabIndex', 'tabindex'], + ['crossOrigin', 'crossorigin'], + // This is a list of all SVG attributes that need special casing. + // Regular attributes that just accept strings.], + ['accentHeight', 'accent-height'], + ['alignmentBaseline', 'alignment-baseline'], + ['arabicForm', 'arabic-form'], + ['baselineShift', 'baseline-shift'], + ['capHeight', 'cap-height'], + ['clipPath', 'clip-path'], + ['clipRule', 'clip-rule'], + ['colorInterpolation', 'color-interpolation'], + ['colorInterpolationFilters', 'color-interpolation-filters'], + ['colorProfile', 'color-profile'], + ['colorRendering', 'color-rendering'], + ['dominantBaseline', 'dominant-baseline'], + ['enableBackground', 'enable-background'], + ['fillOpacity', 'fill-opacity'], + ['fillRule', 'fill-rule'], + ['floodColor', 'flood-color'], + ['floodOpacity', 'flood-opacity'], + ['fontFamily', 'font-family'], + ['fontSize', 'font-size'], + ['fontSizeAdjust', 'font-size-adjust'], + ['fontStretch', 'font-stretch'], + ['fontStyle', 'font-style'], + ['fontVariant', 'font-variant'], + ['fontWeight', 'font-weight'], + ['glyphName', 'glyph-name'], + ['glyphOrientationHorizontal', 'glyph-orientation-horizontal'], + ['glyphOrientationVertical', 'glyph-orientation-vertical'], + ['horizAdvX', 'horiz-adv-x'], + ['horizOriginX', 'horiz-origin-x'], + ['imageRendering', 'image-rendering'], + ['letterSpacing', 'letter-spacing'], + ['lightingColor', 'lighting-color'], + ['markerEnd', 'marker-end'], + ['markerMid', 'marker-mid'], + ['markerStart', 'marker-start'], + ['overlinePosition', 'overline-position'], + ['overlineThickness', 'overline-thickness'], + ['paintOrder', 'paint-order'], + ['panose-1', 'panose-1'], + ['pointerEvents', 'pointer-events'], + ['renderingIntent', 'rendering-intent'], + ['shapeRendering', 'shape-rendering'], + ['stopColor', 'stop-color'], + ['stopOpacity', 'stop-opacity'], + ['strikethroughPosition', 'strikethrough-position'], + ['strikethroughThickness', 'strikethrough-thickness'], + ['strokeDasharray', 'stroke-dasharray'], + ['strokeDashoffset', 'stroke-dashoffset'], + ['strokeLinecap', 'stroke-linecap'], + ['strokeLinejoin', 'stroke-linejoin'], + ['strokeMiterlimit', 'stroke-miterlimit'], + ['strokeOpacity', 'stroke-opacity'], + ['strokeWidth', 'stroke-width'], + ['textAnchor', 'text-anchor'], + ['textDecoration', 'text-decoration'], + ['textRendering', 'text-rendering'], + ['transformOrigin', 'transform-origin'], + ['underlinePosition', 'underline-position'], + ['underlineThickness', 'underline-thickness'], + ['unicodeBidi', 'unicode-bidi'], + ['unicodeRange', 'unicode-range'], + ['unitsPerEm', 'units-per-em'], + ['vAlphabetic', 'v-alphabetic'], + ['vHanging', 'v-hanging'], + ['vIdeographic', 'v-ideographic'], + ['vMathematical', 'v-mathematical'], + ['vectorEffect', 'vector-effect'], + ['vertAdvY', 'vert-adv-y'], + ['vertOriginX', 'vert-origin-x'], + ['vertOriginY', 'vert-origin-y'], + ['wordSpacing', 'word-spacing'], + ['writingMode', 'writing-mode'], + ['xmlnsXlink', 'xmlns:xlink'], + ['xHeight', 'x-height'], +]); + +export default function (name: string): string { + return aliases.get(name) || name; +} diff --git a/packages/react-dom-bindings/src/shared/isUnitlessNumber.js b/packages/react-dom-bindings/src/shared/isUnitlessNumber.js index 334111fa7a385..f965140d05392 100644 --- a/packages/react-dom-bindings/src/shared/isUnitlessNumber.js +++ b/packages/react-dom-bindings/src/shared/isUnitlessNumber.js @@ -10,81 +10,79 @@ /** * CSS properties which accept numbers but are not in units of "px". */ +const unitlessNumbers = new Set([ + 'animationIterationCount', + 'aspectRatio', + 'borderImageOutset', + 'borderImageSlice', + 'borderImageWidth', + 'boxFlex', + 'boxFlexGroup', + 'boxOrdinalGroup', + 'columnCount', + 'columns', + 'flex', + 'flexGrow', + 'flexPositive', + 'flexShrink', + 'flexNegative', + 'flexOrder', + 'gridArea', + 'gridRow', + 'gridRowEnd', + 'gridRowSpan', + 'gridRowStart', + 'gridColumn', + 'gridColumnEnd', + 'gridColumnSpan', + 'gridColumnStart', + 'fontWeight', + 'lineClamp', + 'lineHeight', + 'opacity', + 'order', + 'orphans', + 'scale', + 'tabSize', + 'widows', + 'zIndex', + 'zoom', + 'fillOpacity', // SVG-related properties + 'floodOpacity', + 'stopOpacity', + 'strokeDasharray', + 'strokeDashoffset', + 'strokeMiterlimit', + 'strokeOpacity', + 'strokeWidth', + 'MozAnimationIterationCount', // Known Prefixed Properties + 'MozBoxFlex', // TODO: Remove these since they shouldn't be used in modern code + 'MozBoxFlexGroup', + 'MozLineClamp', + 'msAnimationIterationCount', + 'msFlex', + 'msZoom', + 'msFlexGrow', + 'msFlexNegative', + 'msFlexOrder', + 'msFlexPositive', + 'msFlexShrink', + 'msGridColumn', + 'msGridColumnSpan', + 'msGridRow', + 'msGridRowSpan', + 'WebkitAnimationIterationCount', + 'WebkitBoxFlex', + 'WebKitBoxFlexGroup', + 'WebkitBoxOrdinalGroup', + 'WebkitColumnCount', + 'WebkitColumns', + 'WebkitFlex', + 'WebkitFlexGrow', + 'WebkitFlexPositive', + 'WebkitFlexShrink', + 'WebkitLineClamp', +]); export default function (name: string): boolean { - switch (name) { - case 'animationIterationCount': - case 'aspectRatio': - case 'borderImageOutset': - case 'borderImageSlice': - case 'borderImageWidth': - case 'boxFlex': - case 'boxFlexGroup': - case 'boxOrdinalGroup': - case 'columnCount': - case 'columns': - case 'flex': - case 'flexGrow': - case 'flexPositive': - case 'flexShrink': - case 'flexNegative': - case 'flexOrder': - case 'gridArea': - case 'gridRow': - case 'gridRowEnd': - case 'gridRowSpan': - case 'gridRowStart': - case 'gridColumn': - case 'gridColumnEnd': - case 'gridColumnSpan': - case 'gridColumnStart': - case 'fontWeight': - case 'lineClamp': - case 'lineHeight': - case 'opacity': - case 'order': - case 'orphans': - case 'scale': - case 'tabSize': - case 'widows': - case 'zIndex': - case 'zoom': - case 'fillOpacity': // SVG-related properties - case 'floodOpacity': - case 'stopOpacity': - case 'strokeDasharray': - case 'strokeDashoffset': - case 'strokeMiterlimit': - case 'strokeOpacity': - case 'strokeWidth': - case 'MozAnimationIterationCount': // Known Prefixed Properties - case 'MozBoxFlex': // TODO: Remove these since they shouldn't be used in modern code - case 'MozBoxFlexGroup': - case 'MozLineClamp': - case 'msAnimationIterationCount': - case 'msFlex': - case 'msZoom': - case 'msFlexGrow': - case 'msFlexNegative': - case 'msFlexOrder': - case 'msFlexPositive': - case 'msFlexShrink': - case 'msGridColumn': - case 'msGridColumnSpan': - case 'msGridRow': - case 'msGridRowSpan': - case 'WebkitAnimationIterationCount': - case 'WebkitBoxFlex': - case 'WebKitBoxFlexGroup': - case 'WebkitBoxOrdinalGroup': - case 'WebkitColumnCount': - case 'WebkitColumns': - case 'WebkitFlex': - case 'WebkitFlexGrow': - case 'WebkitFlexPositive': - case 'WebkitFlexShrink': - case 'WebkitLineClamp': - return true; - default: - return false; - } + return unitlessNumbers.has(name); } From 207c626b320cf5b62e08ae3af4aa25076568a6f4 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 4 Apr 2023 13:02:27 -0400 Subject: [PATCH 2/5] Move very common prop names to the beginning of the switch This ensures that when the look up is not optimized, we hit them early in the if sequence. We might actually consider moving these to separate ifs to ensure that even when the switch is optimized to a hash table, these remain as ifs. --- .../src/client/ReactDOMComponent.js | 189 +++++++++++------- .../src/server/ReactDOMServerFormatConfig.js | 63 ++++-- .../src/shared/getAttributeAlias.js | 4 +- 3 files changed, 162 insertions(+), 94 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index b8bb564a330d7..e18561664c582 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -276,35 +276,6 @@ function setProp( props: any, ): void { switch (key) { - case 'style': { - setValueForStyles(domElement, value); - break; - } - case 'dangerouslySetInnerHTML': { - if (value != null) { - if (typeof value !== 'object' || !('__html' in value)) { - throw new Error( - '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + - 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' + - 'for more information.', - ); - } - const nextHtml: any = value.__html; - if (nextHtml != null) { - if (props.children != null) { - throw new Error( - 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', - ); - } - if (disableIEWorkarounds) { - domElement.innerHTML = nextHtml; - } else { - setInnerHTML(domElement, nextHtml); - } - } - } - break; - } case 'children': { if (typeof value === 'string') { // Avoid setting initial textContent when the text is empty. In IE11 setting @@ -325,50 +296,26 @@ function setProp( } break; } - case 'onScroll': { - if (value != null) { - if (__DEV__ && typeof value !== 'function') { - warnForInvalidEventListener(key, value); - } - listenToNonDelegatedEvent('scroll', domElement); - } + // These are very common props and therefore are in the beginning of the switch. + // TODO: aria-label is a very common prop but allows booleans so is not like the others + // but should ideally go in this list too. + case 'className': + setValueForAttribute(domElement, 'class', value); break; - } - case 'onClick': { - // TODO: This cast may not be sound for SVG, MathML or custom elements. - if (value != null) { - if (__DEV__ && typeof value !== 'function') { - warnForInvalidEventListener(key, value); - } - trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); - } - break; - } - // Note: `option.selected` is not updated if `select.multiple` is - // disabled with `removeAttribute`. We have special logic for handling this. - case 'multiple': { - (domElement: any).multiple = - value && typeof value !== 'function' && typeof value !== 'symbol'; - break; - } - case 'muted': { - (domElement: any).muted = - value && typeof value !== 'function' && typeof value !== 'symbol'; + case 'tabIndex': + // This has to be case sensitive in SVG. + setValueForAttribute(domElement, 'tabindex', value); break; - } - case 'suppressContentEditableWarning': - case 'suppressHydrationWarning': - case 'defaultValue': // Reserved - case 'defaultChecked': - case 'innerHTML': { - // Noop + case 'dir': + case 'role': + case 'viewBox': + case 'width': + case 'height': { + setValueForAttribute(domElement, key, value); break; } - case 'autoFocus': { - // We polyfill it separately on the client during commit. - // We could have excluded it in the property list instead of - // adding a special case here, but then it wouldn't be emitted - // on server rendering (but we *do* want to emit it in SSR). + case 'style': { + setValueForStyles(domElement, value); break; } // These attributes accept URLs. These must not allow javascript: URLS. @@ -424,6 +371,77 @@ function setProp( domElement.setAttribute(key, sanitizedValue); break; } + case 'onClick': { + // TODO: This cast may not be sound for SVG, MathML or custom elements. + if (value != null) { + if (__DEV__ && typeof value !== 'function') { + warnForInvalidEventListener(key, value); + } + trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); + } + break; + } + case 'onScroll': { + if (value != null) { + if (__DEV__ && typeof value !== 'function') { + warnForInvalidEventListener(key, value); + } + listenToNonDelegatedEvent('scroll', domElement); + } + break; + } + case 'dangerouslySetInnerHTML': { + if (value != null) { + if (typeof value !== 'object' || !('__html' in value)) { + throw new Error( + '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + + 'Please visit https://reactjs.org/link/dangerously-set-inner-html ' + + 'for more information.', + ); + } + const nextHtml: any = value.__html; + if (nextHtml != null) { + if (props.children != null) { + throw new Error( + 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', + ); + } + if (disableIEWorkarounds) { + domElement.innerHTML = nextHtml; + } else { + setInnerHTML(domElement, nextHtml); + } + } + } + break; + } + // Note: `option.selected` is not updated if `select.multiple` is + // disabled with `removeAttribute`. We have special logic for handling this. + case 'multiple': { + (domElement: any).multiple = + value && typeof value !== 'function' && typeof value !== 'symbol'; + break; + } + case 'muted': { + (domElement: any).muted = + value && typeof value !== 'function' && typeof value !== 'symbol'; + break; + } + case 'suppressContentEditableWarning': + case 'suppressHydrationWarning': + case 'defaultValue': // Reserved + case 'defaultChecked': + case 'innerHTML': { + // Noop + break; + } + case 'autoFocus': { + // We polyfill it separately on the client during commit. + // We could have excluded it in the property list instead of + // adding a special case here, but then it wouldn't be emitted + // on server rendering (but we *do* want to emit it in SSR). + break; + } case 'xlinkHref': { if ( value == null || @@ -789,6 +807,21 @@ export function setInitialProperties( continue; } switch (propKey) { + case 'type': { + // Fast path since 'type' is very common on inputs + if ( + propValue != null && + typeof propValue !== 'function' && + typeof propValue !== 'symbol' && + typeof propValue !== 'boolean' + ) { + if (__DEV__) { + checkAttributeStringCoercion(propValue, propKey); + } + domElement.setAttribute(propKey, propValue); + } + break; + } case 'checked': { const node = ((domElement: any): InputWithWrapperState); const checked = @@ -1805,6 +1838,24 @@ function diffHydratedGenericElement( warnForPropDifference(propKey, serverHTML, expectedHTML); } continue; + case 'className': + hydrateAttribute( + domElement, + propKey, + 'class', + value, + extraAttributes, + ); + continue; + case 'tabIndex': + hydrateAttribute( + domElement, + propKey, + 'tabindex', + value, + extraAttributes, + ); + continue; case 'style': extraAttributes.delete(propKey); diffHydratedStyles(domElement, value); diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 3b8e876268f8f..8d125ced4be28 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -641,23 +641,29 @@ function pushAttribute( value: string | boolean | number | Function | Object, // not null or undefined ): void { switch (name) { + // These are very common props and therefore are in the beginning of the switch. + // TODO: aria-label is a very common prop but allows booleans so is not like the others + // but should ideally go in this list too. + case 'className': { + pushStringAttribute(target, 'class', value); + break; + } + case 'tabIndex': { + pushStringAttribute(target, 'tabindex', value); + break; + } + case 'dir': + case 'role': + case 'viewBox': + case 'width': + case 'height': { + pushStringAttribute(target, name, value); + break; + } case 'style': { pushStyleAttribute(target, value); return; } - case 'defaultValue': - case 'defaultChecked': // These shouldn't be set as attributes on generic HTML elements. - case 'innerHTML': // Must use dangerouslySetInnerHTML instead. - case 'suppressContentEditableWarning': - case 'suppressHydrationWarning': - // Ignored. These are built-in to React on the client. - return; - case 'autoFocus': - case 'multiple': - case 'muted': { - pushBooleanAttribute(target, name.toLowerCase(), value); - return; - } case 'src': case 'href': case 'action': @@ -710,6 +716,19 @@ function pushAttribute( ); return; } + case 'defaultValue': + case 'defaultChecked': // These shouldn't be set as attributes on generic HTML elements. + case 'innerHTML': // Must use dangerouslySetInnerHTML instead. + case 'suppressContentEditableWarning': + case 'suppressHydrationWarning': + // Ignored. These are built-in to React on the client. + return; + case 'autoFocus': + case 'multiple': + case 'muted': { + pushBooleanAttribute(target, name.toLowerCase(), value); + return; + } case 'xlinkHref': { if ( typeof value === 'function' || @@ -849,31 +868,31 @@ function pushAttribute( } case 'xlinkActuate': pushStringAttribute(target, 'xlink:actuate', value); - break; + return; case 'xlinkArcrole': pushStringAttribute(target, 'xlink:arcrole', value); - break; + return; case 'xlinkRole': pushStringAttribute(target, 'xlink:role', value); - break; + return; case 'xlinkShow': pushStringAttribute(target, 'xlink:show', value); - break; + return; case 'xlinkTitle': pushStringAttribute(target, 'xlink:title', value); - break; + return; case 'xlinkType': pushStringAttribute(target, 'xlink:type', value); - break; + return; case 'xmlBase': pushStringAttribute(target, 'xml:base', value); - break; + return; case 'xmlLang': pushStringAttribute(target, 'xml:lang', value); - break; + return; case 'xmlSpace': pushStringAttribute(target, 'xml:space', value); - break; + return; default: if ( // shouldIgnoreAttribute diff --git a/packages/react-dom-bindings/src/shared/getAttributeAlias.js b/packages/react-dom-bindings/src/shared/getAttributeAlias.js index 07add09914ec1..879c20a69453a 100644 --- a/packages/react-dom-bindings/src/shared/getAttributeAlias.js +++ b/packages/react-dom-bindings/src/shared/getAttributeAlias.js @@ -9,11 +9,9 @@ const aliases = new Map([ ['acceptCharset', 'accept-charset'], - ['className', 'class'], ['htmlFor', 'for'], ['httpEquiv', 'http-equiv'], - // HTML and SVG attributes, but the SVG attribute is sensitive.], - ['tabIndex', 'tabindex'], + // HTML and SVG attributes, but the SVG attribute is sensitive.], ['crossOrigin', 'crossorigin'], // This is a list of all SVG attributes that need special casing. // Regular attributes that just accept strings.], From 9c208e3b16af1fd485eb3ad0be98722fac10b84b Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 4 Apr 2023 13:10:38 -0400 Subject: [PATCH 3/5] Special case known attribute helper that can by pass the safe checks --- .../src/client/DOMPropertyOperations.js | 27 +++++++++++++++++++ .../src/client/ReactDOMComponent.js | 15 ++++------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js index 4066055e0197f..ac311c72e01b6 100644 --- a/packages/react-dom-bindings/src/client/DOMPropertyOperations.js +++ b/packages/react-dom-bindings/src/client/DOMPropertyOperations.js @@ -140,6 +140,33 @@ export function setValueForAttribute( } } +export function setValueForKnownAttribute( + node: Element, + name: string, + value: mixed, +) { + if (value === null) { + node.removeAttribute(name); + return; + } + switch (typeof value) { + case 'undefined': + case 'function': + case 'symbol': + case 'boolean': { + node.removeAttribute(name); + return; + } + } + if (__DEV__) { + checkAttributeStringCoercion(value, name); + } + node.setAttribute( + name, + enableTrustedTypesIntegration ? (value: any) : '' + (value: any), + ); +} + export function setValueForNamespacedAttribute( node: Element, namespace: string, diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index e18561664c582..ca85e019f3b2d 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -22,6 +22,7 @@ import { getValueForAttribute, getValueForAttributeOnCustomComponent, setValueForPropertyOnCustomComponent, + setValueForKnownAttribute, setValueForAttribute, setValueForNamespacedAttribute, } from './DOMPropertyOperations'; @@ -300,18 +301,18 @@ function setProp( // TODO: aria-label is a very common prop but allows booleans so is not like the others // but should ideally go in this list too. case 'className': - setValueForAttribute(domElement, 'class', value); + setValueForKnownAttribute(domElement, 'class', value); break; case 'tabIndex': // This has to be case sensitive in SVG. - setValueForAttribute(domElement, 'tabindex', value); + setValueForKnownAttribute(domElement, 'tabindex', value); break; case 'dir': case 'role': case 'viewBox': case 'width': case 'height': { - setValueForAttribute(domElement, key, value); + setValueForKnownAttribute(domElement, key, value); break; } case 'style': { @@ -1839,13 +1840,7 @@ function diffHydratedGenericElement( } continue; case 'className': - hydrateAttribute( - domElement, - propKey, - 'class', - value, - extraAttributes, - ); + hydrateAttribute(domElement, propKey, 'class', value, extraAttributes); continue; case 'tabIndex': hydrateAttribute( From 9afe183a8a15bb1a7c16e1f9bb350def649f0f16 Mon Sep 17 00:00:00 2001 From: Sebastian Markbage Date: Tue, 4 Apr 2023 13:18:06 -0400 Subject: [PATCH 4/5] Fast track very common tag names so we can match them with plain ifs --- .../src/client/ReactDOMComponent.js | 96 ++++++++++++------- .../src/server/ReactDOMServerFormatConfig.js | 17 +++- 2 files changed, 77 insertions(+), 36 deletions(-) diff --git a/packages/react-dom-bindings/src/client/ReactDOMComponent.js b/packages/react-dom-bindings/src/client/ReactDOMComponent.js index ca85e019f3b2d..20395d09ac090 100644 --- a/packages/react-dom-bindings/src/client/ReactDOMComponent.js +++ b/packages/react-dom-bindings/src/client/ReactDOMComponent.js @@ -794,6 +794,17 @@ export function setInitialProperties( // TODO: Make sure that we check isMounted before firing any of these events. switch (tag) { + case 'div': + case 'span': + case 'svg': + case 'path': + case 'a': + case 'g': + case 'p': + case 'li': { + // Fast track the most common tag types + break; + } case 'input': { ReactDOMInputInitWrapperState(domElement, props); // We listen to this event in case to ensure emulated bubble @@ -1037,30 +1048,32 @@ export function setInitialProperties( } return; } + default: { + if (isCustomElement(tag, props)) { + for (const propKey in props) { + if (!props.hasOwnProperty(propKey)) { + continue; + } + const propValue = props[propKey]; + if (propValue == null) { + continue; + } + setPropOnCustomElement(domElement, tag, propKey, propValue, props); + } + return; + } + } } - if (isCustomElement(tag, props)) { - for (const propKey in props) { - if (!props.hasOwnProperty(propKey)) { - continue; - } - const propValue = props[propKey]; - if (propValue == null) { - continue; - } - setPropOnCustomElement(domElement, tag, propKey, propValue, props); + for (const propKey in props) { + if (!props.hasOwnProperty(propKey)) { + continue; } - } else { - for (const propKey in props) { - if (!props.hasOwnProperty(propKey)) { - continue; - } - const propValue = props[propKey]; - if (propValue == null) { - continue; - } - setProp(domElement, tag, propKey, propValue, props); + const propValue = props[propKey]; + if (propValue == null) { + continue; } + setProp(domElement, tag, propKey, propValue, props); } } @@ -1186,6 +1199,17 @@ export function updateProperties( nextProps: Object, ): void { switch (tag) { + case 'div': + case 'span': + case 'svg': + case 'path': + case 'a': + case 'g': + case 'p': + case 'li': { + // Fast track the most common tag types + break; + } case 'input': { // Update checked *before* name. // In the middle of an update, it is possible to have multiple checked. @@ -1343,21 +1367,29 @@ export function updateProperties( } return; } + default: { + if (isCustomElement(tag, nextProps)) { + for (let i = 0; i < updatePayload.length; i += 2) { + const propKey = updatePayload[i]; + const propValue = updatePayload[i + 1]; + setPropOnCustomElement( + domElement, + tag, + propKey, + propValue, + nextProps, + ); + } + return; + } + } } // Apply the diff. - if (isCustomElement(tag, nextProps)) { - for (let i = 0; i < updatePayload.length; i += 2) { - const propKey = updatePayload[i]; - const propValue = updatePayload[i + 1]; - setPropOnCustomElement(domElement, tag, propKey, propValue, nextProps); - } - } else { - for (let i = 0; i < updatePayload.length; i += 2) { - const propKey = updatePayload[i]; - const propValue = updatePayload[i + 1]; - setProp(domElement, tag, propKey, propValue, nextProps); - } + for (let i = 0; i < updatePayload.length; i += 2) { + const propKey = updatePayload[i]; + const propValue = updatePayload[i + 1]; + setProp(domElement, tag, propKey, propValue, nextProps); } } diff --git a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js index 8d125ced4be28..2df5fb332fa1d 100644 --- a/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js +++ b/packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js @@ -2638,6 +2638,16 @@ export function pushStartInstance( } switch (type) { + case 'div': + case 'span': + case 'svg': + case 'path': + case 'a': + case 'g': + case 'p': + case 'li': + // Fast track very common tags + break; // Special tags case 'select': return pushStartSelect(target, props); @@ -2747,15 +2757,14 @@ export function pushStartInstance( ); } default: { - if (type.indexOf('-') === -1) { - // Generic element - return pushStartGenericElement(target, props, type); - } else { + if (type.indexOf('-') !== -1) { // Custom element return pushStartCustomElement(target, props, type); } } } + // Generic element + return pushStartGenericElement(target, props, type); } const endTag1 = stringToPrecomputedChunk(' Date: Tue, 4 Apr 2023 17:02:55 -0400 Subject: [PATCH 5/5] Fix typo Co-authored-by: Sophie Alpert --- packages/react-dom-bindings/src/shared/getAttributeAlias.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-dom-bindings/src/shared/getAttributeAlias.js b/packages/react-dom-bindings/src/shared/getAttributeAlias.js index 879c20a69453a..2ed432f20275b 100644 --- a/packages/react-dom-bindings/src/shared/getAttributeAlias.js +++ b/packages/react-dom-bindings/src/shared/getAttributeAlias.js @@ -11,7 +11,7 @@ const aliases = new Map([ ['acceptCharset', 'accept-charset'], ['htmlFor', 'for'], ['httpEquiv', 'http-equiv'], - // HTML and SVG attributes, but the SVG attribute is sensitive.], + // HTML and SVG attributes, but the SVG attribute is case sensitive.], ['crossOrigin', 'crossorigin'], // This is a list of all SVG attributes that need special casing. // Regular attributes that just accept strings.],