From ce188d152006dd3106e700acc5cd22c9d2075256 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Mon, 12 May 2025 10:12:09 +0200 Subject: [PATCH] perf(material/form-field): split DOM accesses into read and write In the outlined appearance we have to check the width of prefixes and suffixes and then write it to the floating label. These changes split it into `read` and `write` phases to reduce the amount of layout thrashing. --- src/material/form-field/form-field.ts | 77 ++++++++++++++++----------- 1 file changed, 47 insertions(+), 30 deletions(-) diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts index c3c81b054553..c737e5e2eed9 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -106,6 +106,11 @@ export const MAT_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken { - if (this._appearanceSignal() === 'outline') { - this._updateOutlineLabelOffset(); - if (!globalThis.ResizeObserver) { - return; + afterRenderEffect({ + earlyRead: () => { + if (this._appearanceSignal() !== 'outline') { + this._outlineLabelOffsetResizeObserver?.disconnect(); + return null; } // Setup a resize observer to monitor changes to the size of the prefix / suffix and // readjust the label offset. - this._outlineLabelOffsetResizeObserver ||= new globalThis.ResizeObserver(() => - this._updateOutlineLabelOffset(), - ); - for (const el of this._prefixSuffixContainers()) { - this._outlineLabelOffsetResizeObserver.observe(el, {box: 'border-box'}); + if (globalThis.ResizeObserver) { + this._outlineLabelOffsetResizeObserver ||= new globalThis.ResizeObserver(() => { + this._writeOutlinedLabelStyles(this._getOutlinedLabelOffset()); + }); + for (const el of this._prefixSuffixContainers()) { + this._outlineLabelOffsetResizeObserver.observe(el, {box: 'border-box'}); + } } - } else { - this._outlineLabelOffsetResizeObserver?.disconnect(); - } + + return this._getOutlinedLabelOffset(); + }, + write: labelStyles => this._writeOutlinedLabelStyles(labelStyles()), }); } @@ -740,7 +745,7 @@ export class MatFormField } /** - * Updates the horizontal offset of the label in the outline appearance. In the outline + * Calculates the horizontal offset of the label in the outline appearance. In the outline * appearance, the notched-outline and label are not relative to the infix container because * the outline intends to surround prefixes, suffixes and the infix. This means that the * floating label by default overlaps prefixes in the docked state. To avoid this, we need to @@ -748,22 +753,20 @@ export class MatFormField * not need to do this because they use a fixed width for prefixes. Hence, they can simply * incorporate the horizontal offset into their default text-field styles. */ - private _updateOutlineLabelOffset() { + private _getOutlinedLabelOffset(): OutlinedLabelStyles { const dir = this._dir.valueSignal(); if (!this._hasOutline() || !this._floatingLabel) { - return; + return null; } - const floatingLabel = this._floatingLabel.element; // If no prefix is displayed, reset the outline label offset from potential // previous label offset updates. - if (!(this._iconPrefixContainer || this._textPrefixContainer)) { - floatingLabel.style.transform = ''; - return; + if (!this._iconPrefixContainer && !this._textPrefixContainer) { + return ['', null]; } // If the form field is not attached to the DOM yet (e.g. in a tab), we defer // the label offset update until the zone stabilizes. if (!this._isAttachedToDom()) { - return; + return null; } const iconPrefixContainer = this._iconPrefixContainer?.nativeElement; const textPrefixContainer = this._textPrefixContainer?.nativeElement; @@ -783,19 +786,33 @@ export class MatFormField // Update the translateX of the floating label to account for the prefix container, // but allow the CSS to override this setting via a CSS variable when the label is // floating. - floatingLabel.style.transform = `var( - --mat-mdc-form-field-label-transform, - ${FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM} translateX(${labelHorizontalOffset}) - )`; + const floatingLabelTransform = + 'var(--mat-mdc-form-field-label-transform, ' + + `${FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM} translateX(${labelHorizontalOffset}))`; // Prevent the label from overlapping the suffix when in resting position. - const prefixAndSuffixWidth = + const notchedOutlineWidth = iconPrefixContainerWidth + textPrefixContainerWidth + iconSuffixContainerWidth + textSuffixContainerWidth; - this._notchedOutline?._setMaxWidth(prefixAndSuffixWidth); + return [floatingLabelTransform, notchedOutlineWidth]; + } + + /** Writes the styles produced by `_getOutlineLabelOffset` synchronously to the DOM. */ + private _writeOutlinedLabelStyles(styles: OutlinedLabelStyles): void { + if (styles !== null) { + const [floatingLabelTransform, notchedOutlineWidth] = styles; + + if (this._floatingLabel) { + this._floatingLabel.element.style.transform = floatingLabelTransform; + } + + if (notchedOutlineWidth !== null) { + this._notchedOutline?._setMaxWidth(notchedOutlineWidth); + } + } } /** Checks whether the form field is attached to the DOM. */