Skip to content

perf(forms): defer element.focus() to next animation frame#68884

Open
arturovt wants to merge 1 commit into
angular:mainfrom
arturovt:perf/forms_defer_focus
Open

perf(forms): defer element.focus() to next animation frame#68884
arturovt wants to merge 1 commit into
angular:mainfrom
arturovt:perf/forms_defer_focus

Conversation

@arturovt
Copy link
Copy Markdown
Contributor

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:

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.

Screenshot from 2026-05-22 18-31-59 Screenshot from 2026-05-22 18-32-38

@pullapprove pullapprove Bot requested a review from crisbeto May 22, 2026 15:40
@angular-robot angular-robot Bot added area: performance Issues related to performance area: forms labels May 22, 2026
@ngbot ngbot Bot added this to the Backlog milestone May 22, 2026
*/
private focuser = (options?: FocusOptions) => this.element.focus(options);
private focuser = (options?: FocusOptions) => {
if (ngServerMode || typeof requestAnimationFrame !== 'function') {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: To make an assertion for requestAnimationFrame, shouldn't we focus only on when it's available? Perhaps we should have a fallback?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(My understanding might be lost in translation)

Copy link
Copy Markdown
Contributor

@SkyZeroZx SkyZeroZx May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@arturovt arturovt force-pushed the perf/forms_defer_focus branch from d4cade0 to f7e1be2 Compare May 22, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: forms area: performance Issues related to performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants