perf(forms): defer element.focus() to next animation frame#68884
perf(forms): defer element.focus() to next animation frame#68884arturovt wants to merge 1 commit into
Conversation
| */ | ||
| private focuser = (options?: FocusOptions) => this.element.focus(options); | ||
| private focuser = (options?: FocusOptions) => { | ||
| if (ngServerMode || typeof requestAnimationFrame !== 'function') { |
There was a problem hiding this comment.
Q: To make an assertion for requestAnimationFrame, shouldn't we focus only on when it's available? Perhaps we should have a fallback?
There was a problem hiding this comment.
Do you mean this:
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => {
this.element.focus(options);
});
}Or this:
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => {
this.element.focus(options);
});
} else {
this.element.focus(options);
}There was a problem hiding this comment.
(My understanding might be lost in translation)
There was a problem hiding this comment.
I'm referring to the second one.
I would think something like this would be ideal
if (typeof requestAnimationFrame === 'function') {
requestAnimationFrame(() => {
this.element.focus(options);
});
} else {
this.element.focus(options);
}, because if for some reason requestAnimationFrame were not defined, focus would never be given to the application user.
Or perhaps I'm overlooking something?
There was a problem hiding this comment.
Yes, the second one is better since it covers testing scenarios where rAF might be unavailable. I'll update.
Calling `element.focus()` synchronously can be surprisingly expensive because the browser must immediately flush any pending style recalculation and layout work before it can determine whether the element is focusable. When this happens in the middle of DOM mutations, all pending work since the last frame gets forced through at once, blocking the main thread.
This change defers the `focus()` call to `requestAnimationFrame`, allowing the browser to complete its normal pre-paint lifecycle first (style recalculation, layout tree updates, and geometry layout). By the time `focus()` runs, the document is already in a clean state, making the call much cheaper.
Measured on a checkout form coupon-code field:
```ts id="ob6h7x"
const t0 = performance.now();
this.checkoutForm.couponCode().focusBoundControl();
const t1 = performance.now();
performance.measure('focus()', { start: t0, end: t1 });
```
Before: `~60ms`
After: `~20ms`
The `typeof` guard preserves compatibility with server-side rendering environments where `requestAnimationFrame` is not available.
d4cade0 to
f7e1be2
Compare
Calling
element.focus()synchronously can be surprisingly expensive because the browser must immediately flush any pending style recalculation and layout work before it can determine whether the element is focusable. When this happens in the middle of DOM mutations, all pending work since the last frame gets forced through at once, blocking the main thread.This change defers the
focus()call torequestAnimationFrame, allowing the browser to complete its normal pre-paint lifecycle first (style recalculation, layout tree updates, and geometry layout). By the timefocus()runs, the document is already in a clean state, making the call much cheaper.Measured on a checkout form coupon-code field:
Before:
~60msAfter:
~20msThe
typeofguard preserves compatibility with server-side rendering environments whererequestAnimationFrameis not available.