Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EuiFlyout] Performance cleanup #7462

Merged
merged 9 commits into from
Mar 14, 2024

Conversation

davismcphee
Copy link
Contributor

@davismcphee davismcphee commented Jan 14, 2024

Summary

This PR includes two changes which optimize browser reflows when using EuiResizableFlyout, which has a significant impact on the performance of the flyout in heavy pages such as Discover:

  • Switched from the useResizeObserver hook to a ResizeObserver directly to track the flyout size, since the use of getBoundingClientRect in useResizeObserver causes layout thrashing when combined with the body padding modifications made by the flyout.
  • No longer toggle the euiBody--hasFlyout class, or remove/re-add body padding whenever the flyout size changes. Instead the class is only added/removed on mount/unmount, and the body padding is only updated on resize and removed on unmount.

In my tests of frantically resizing the Discover flyout for 10 seconds while profiling, these changes brought the time Chrome spent on Recalculate Style from 4.8s to 0.8s:

Unoptimized:
unoptimized

Optimized:
optimized

And a couple of videos from Discover for reference:

Unoptimized:

unoptimized.mp4

Optimized:

chromium.mp4

The optimizations also improve the performance in Firefox and Safari. Performance in Safari is still a bit slow compared to the other browsers, but it's acceptable and at least much better than before.

Firefox:

firefox.mp4

Safari:

safari.mp4

QA

General checklist

  • Browser QA
    • Checked in both light and dark modes
    • Checked in mobile
    • Checked in Chrome, Safari, Edge, and Firefox
      - [ ] Checked for accessibility including keyboard-only and screenreader modes
  • Docs site QA - N/A
  • Code quality checklist
  • Release checklist
    • A changelog entry exists and is marked appropriately.
      - [ ] If applicable, added the breaking change issue label (and filled out the breaking change checklist)
  • Designer checklist - N/A

expect(add).toHaveBeenLastCalledWith('euiBody--hasFlyout');
});

it('should not remove and re-add "euiBody--hasFlyout" class on resize', async () => {
Copy link
Contributor Author

@davismcphee davismcphee Jan 14, 2024

Choose a reason for hiding this comment

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

The changes are mainly an optimization which is sort of hard to test, so instead I wrote some tests for the euiBody--hasFlyout class and included this test case that would otherwise fail without the optimization.

@cee-chen
Copy link
Contributor

cee-chen commented Feb 5, 2024

Hey @davismcphee! Just wanted to check in on this PR - it looks great to me honestly, should I go ahead and mark its as ready for review + add a changelog to it? Happy to take over and bring it across the finish line if you're busy with other things!

@kertal
Copy link
Member

kertal commented Feb 7, 2024

We are currently evaluating EuiDataGrid for the UnifiedDocViewer, and created an instance which applies resizable flyouts and a basic migration of the DocViewer, to measure the performance impact

your can test is here
https://kertal-pr-176273-doc-viewer-push-and-eui-data-grid.kbndev.co/app/discover#/?_g=(filters:!(),refreshInterval:(pause:!t,value:60000),time:(from:now-15m,to:now))&_a=(columns:!(),filters:!(),index:ff959d40-b880-11e8-a6d9-e546fe2bba5f,interval:auto,query:(language:kuery,query:''),sort:!(!(order_date,desc)))

We merged also this fix in this deployment, it resolved the issue in Chrome 🥳 , but still, Firefox + Safari 😿 don't provide the expected performance, Chrome - Left, Firefox - Right

Kapture.2024-02-07.at.10.18.22.mp4

@cee-chen
Copy link
Contributor

cee-chen commented Feb 7, 2024

Adding a throttler/debouncer to the width changes would be the next step to improving performance - would that be something y'all would like to request as a feature/prop as well?

@kertal
Copy link
Member

kertal commented Feb 8, 2024

Adding a throttler/debouncer to the width changes would be the next step to improving performance - would that be something y'all would like to request as a feature/prop as well?

@cee-chen is this the reason for the performance difference between Chrome and the rest of the gang? If this is the case I think throttling resizing should be applied (not optional)

@cee-chen
Copy link
Contributor

cee-chen commented Feb 8, 2024

It's the most obvious approach that I can think of, but I have no idea if that's the difference between browsers, and it isn't guaranteed to fix the issue per se.

The other approach would be to check/optimize the number of rerenders on the rest of the page that's reflowing, as that's likely the culprit of repaint/reflow performance issues, but that's obviously far more involved & harder to do.

…and styles to be toggled multiple times each render, resulting in a lot of extra reflows in the browser
@davismcphee davismcphee force-pushed the optimize-resizable-flyout-reflows branch from b6a5a04 to ef70c47 Compare March 4, 2024 05:01
@davismcphee davismcphee force-pushed the optimize-resizable-flyout-reflows branch from 482b9ef to f85b22f Compare March 4, 2024 22:38
@davismcphee davismcphee marked this pull request as ready for review March 4, 2024 22:40
@davismcphee davismcphee requested a review from a team as a code owner March 4, 2024 22:40
@davismcphee
Copy link
Contributor Author

@cee-chen Apologies for taking so long getting back to you on this one! I finally had a chance to revisit this PR and track down the main culprit for the performance issue.

It turns out that in addition to toggling the euiBody--hasFlyout hurting performance, the most impactful issue was the useResizeObserver hook. Internally it uses getBoundingClientRect, which causes layout thrashing when combined with frequent writes to DOM elements, like when we set the body padding on resize. To fix it I updated EuiFlyout to use a ResizeObserver directly instead of depending on the hook, which resolves the performance issue in all browsers.

It looks like useResizeObserver relies on getBoundingClientRect because contentRect only provides the content sizing for elements instead of box sizing:

// `entry.contentRect` provides incomplete `height` and `width` data.
// Use `getBoundingClientRect` to account for padding and border.
// https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly
const { height, width } = container.getBoundingClientRect();

But I assume this code is old and isn't actually needed anymore since ResizeObserverEntry now supports borderBoxSize to access box sizing. So technically this could have been solved directly within useResizeObserver, but I ended up just addressing EuiFlyout since I wasn't sure if we'd be worried about the impact of changing the hook (although it should technically work the same).

@cee-chen
Copy link
Contributor

cee-chen commented Mar 7, 2024

It looks like useResizeObserver relies on getBoundingClientRect because contentRect only provides the content sizing for elements instead of box sizing:
[...]
But I assume this code is old and isn't actually needed anymore since ResizeObserverEntry now supports borderBoxSize to access box sizing. So technically this could have been solved directly within useResizeObserver, but I ended up just addressing EuiFlyout since I wasn't sure if we'd be worried about the impact of changing the hook (although it should technically work the same).

Super interesting, thanks for discovering this Davis! I actually think this is important enough to address at the useResizeObserver level rather than just for EuiFlyout. I'd be open to another PR that fixes this for all usages of our resize observer(s) as I think that should greatly improve performance across the board.

WDYT?

@davismcphee
Copy link
Contributor Author

@cee-chen Thanks for opening #7575! I think it's a great idea to implement this at the resize observer level. We'll probably even see performance gains elsewhere in Kibana just from that.

For this PR, the resize observer change would of course need to be reverted, but would you be open to still taking the original euiBody--hasFlyout class optimization? It seemed less impactful in Firefox and Safari in my tests, but still had a noticeable improvement on performance in Chrome.

@cee-chen
Copy link
Contributor

@davismcphee Yes, 100%, I definitely want that change! 😄

- add a ternary to *not* instantiate the resize observer if this isn't a push flyout (addresses the old TODO)

- DRY out `paddingSide` logic more

- move body className comment to multiline format (file organization)
- the body class code is now much simpler, so we can adjust tests accordingly
@cee-chen
Copy link
Contributor

cee-chen commented Mar 13, 2024

@davismcphee Alrighty, I've updated the PR with latest main and pushed up the minor changes I had - did you want to re-test the performance impacts to make sure everything's still shipshape, or is this good to merge in your opinion?

@cee-chen cee-chen changed the title [EuiResizableFlyout] Optimize resizable flyout browser reflows [EuiFlyout] Performance cleanup Mar 13, 2024
@kibanamachine
Copy link

Preview staging links for this PR:

@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

Copy link
Contributor Author

@davismcphee davismcphee left a comment

Choose a reason for hiding this comment

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

@cee-chen I gave it another try in Discover with the resize observer fix and latest changes, and I can confirm performance is good in Chrome, Firefox, and Safari (relatively)!


document.body.style[paddingSide] = `${width}px`;
return () => {
document.body.style[paddingSide] = '';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I noticed while testing this gets called on each frame as the width changes when resizing, but it didn't seem to have much if any performance impact in Chrome, Firefox, or Safari.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks a million for checking that Davis! 🙇

Copy link
Contributor

@cee-chen cee-chen left a comment

Choose a reason for hiding this comment

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

Merging - thanks again for your awesome work Davis, you rock!! ❤️‍🔥

@cee-chen cee-chen merged commit f4e7570 into elastic:main Mar 14, 2024
11 checks passed
@davismcphee davismcphee deleted the optimize-resizable-flyout-reflows branch March 15, 2024 00:24
cee-chen added a commit to elastic/kibana that referenced this pull request Mar 22, 2024
`v93.3.0`⏩ `v93.4.0`

---

## [`v93.4.0`](https://github.com/elastic/eui/releases/v93.4.0)

- Added the following properties to `EuiButtonGroup`'s `options`
configs: `toolTipContent`, `toolTipProps`, and `title`. These new
properties allow wrapping buttons in `EuiToolTips`, and additionally
customizing or disabling the native browser `title` tooltip.
([#7461](elastic/eui#7461))
- Enhanced `EuiResizeObserver` and `useResizeObserver`'s performance to
not trigger page reflows on resize event
([#7575](elastic/eui#7575))
- Updated `EuiSuperUpdateButton` to support custom button text via an
optional `children` prop
([#7576](elastic/eui#7576))

**Bug fixes**

- Fixed `EuiFlyout` to not repeatedly remove/add a body class on resize
([#7462](elastic/eui#7462))
- Fixed `EuiToast` title text to wrap instead of overflowing out of the
container ([#7568](elastic/eui#7568))
- Fixed a visual bug with `EuiHeaderBreadcrumbs` with popovers
([#7580](elastic/eui#7580))

**Deprecations**

- Deprecated `euiPalettePositive` and `euiPaletteNegative` in favour of
a more culturally inclusive `euiPaletteGreen` and `euiPaletteRed`
([#7570](elastic/eui#7570))
- Deprecated all charts theme exports in favor of `@elastic/charts`
exports: ([#7572](elastic/eui#7572))
- Deprecated `EUI_CHARTS_THEME_<DARK|LIGHT>` in favor of
`<DARK|LIGHT>_THEME` from `@elastic/charts`.
([#7572](elastic/eui#7572))
- Deprecated `EUI_SPARKLINE_THEME_PARTIAL` in favor of
`useSparklineOverrides` theme from the kibana `charts` plugin `theme`
service.

**Accessibility**

- Updated `EuiModal` to set an `aria-modal` attribute and a default
`dialog` role ([#7564](elastic/eui#7564))
- Updated `EuiConfirmModal` to set a default `alertdialog` role
([#7564](elastic/eui#7564))
- Fixed `EuiModal` and `EuiConfirmModal` to properly trap
Safari+VoiceOver's virtual cursor
([#7564](elastic/eui#7564))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants