Skip to content

Commit 5a40712

Browse files
authored
fix(assignee-selector): split autofocus inp cost over multiple frames (#64165)
This PR reduces the INP impact of the autofocus react attribute by deferring the focus event until next frame. Calling [.focus](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) causes layout thrashing which causes UI jank. We are currently able to observe this using browser profiling where a lot of the call stacks point towards a .focus perf culprit. ![CleanShot 2024-01-30 at 09 29 07@2x](https://github.com/getsentry/sentry/assets/9317857/98e5b8dc-565c-4b3f-9106-5dcf032a46ef) Having looked at prod profiles and react source, the function responsible for calling .focus is [commitMount](https://github.com/facebook/react/blob/2477384650bd184d3ac4a881130118f2636f8551/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js#L676), called during a finalizeInitialChildren. I did not verify when exactly this is called, but from the profiling data, this always appears to always be the last function in the call stacks when committing changes to the DOM. The PR mitigates the sync nature of the .focus call by removing the autofocus attribute and scheduling the autofocus to be performed in the next browser event loop tick, yielding to the engine and splitting the cost over two multiple browser frames.
1 parent d1b76c5 commit 5a40712

File tree

1 file changed

+28
-3
lines changed
  • static/app/components/dropdownAutoComplete

1 file changed

+28
-3
lines changed

static/app/components/dropdownAutoComplete/menu.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,26 @@ export type MenuFooterChildProps = {
2222

2323
type ListProps = React.ComponentProps<typeof List>;
2424

25+
// autoFocus react attribute is sync called on render, this causes
26+
// layout thrashing and is bad for performance. This thin wrapper function
27+
// will defer the focus call until the next frame, after the browser and react
28+
// have had a chance to update the DOM, splitting the perf cost across frames.
29+
function focusElement(targetRef: HTMLElement | null) {
30+
if (!targetRef) {
31+
return;
32+
}
33+
34+
if ('requestAnimationFrame' in window) {
35+
window.requestAnimationFrame(() => {
36+
targetRef.focus();
37+
});
38+
} else {
39+
setTimeout(() => {
40+
targetRef.focus();
41+
}, 1);
42+
}
43+
}
44+
2545
export interface MenuProps
2646
extends Pick<
2747
ListProps,
@@ -121,7 +141,7 @@ export interface MenuProps
121141
/**
122142
* Props to pass to input/filter component
123143
*/
124-
inputProps?: {style: React.CSSProperties};
144+
inputProps?: React.HTMLAttributes<HTMLInputElement>;
125145

126146
/**
127147
* Used to control the input value (optional)
@@ -343,13 +363,18 @@ function Menu({
343363
<StyledDropdownBubble
344364
className={className}
345365
{...getMenuProps(menuProps)}
346-
{...{style, css, blendCorner, detached, alignMenu, minWidth}}
366+
style={style}
367+
css={css}
368+
blendCorner={blendCorner}
369+
detached={detached}
370+
alignMenu={alignMenu}
371+
minWidth={minWidth}
347372
>
348373
<DropdownMainContent minWidth={minWidth}>
349374
{showInput && (
350375
<InputWrapper>
351376
<StyledInput
352-
autoFocus
377+
ref={focusElement}
353378
placeholder={searchPlaceholder}
354379
{...getInputProps({...inputProps, onChange})}
355380
/>

0 commit comments

Comments
 (0)