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

[SearchBar] Improve rendering performance #119189

Merged

Conversation

Dosant
Copy link
Contributor

@Dosant Dosant commented Nov 19, 2021

Summary

This pr is set of small UI performance improvements for our SearchBar component.
This is the result of my space time where I explored different react performance improvements techniques. doc

Some of the changes:

  • To avoid redundant re-renders:
    • Wrap component in React.memo (or use PureComponent) where it makes sense. For example, we don't need to re-run a render cycle on <FilterBar/> when user types into QueryInput
    • Avoid calling setState and changing the object ref when it is not needed to avoid re-rendering
    • Wrap handlers into useCallback or move them as class method to get a stable reference
    • Wrap some values into useMemo to get a stable reference
  • Fix slow rendering:
    • Split large components into smaller once to utilize memoization (e.g. see<SharingMetaFields />)
    • Avoid reading layout values from DOM elements during render. (see layout thrashing). For this had to refactor position tracking for suggestions.

Screen Shot 2021-12-07 at 15 19 03

  • Improve event handlers performance:
    • Debounce event handlers: wrap some of event handlers into requestAnimationFrame scheduler. This is especially beneficial for those that read layout values from DOM elements. (e.g. scrollIntoView in <SuggestionsComponent/>)

Screen Shot 2021-12-07 at 15 19 11

Risk Matrix

Risk Probability Severity Mitigation/Notes
Bugs with suggestions positioning due to refactor Mid Mid Suggestions are used as part of a top search bar, but also, for example, in visualize app when configuring filters aggregation.
Query history regressions Low Low This was moved around a bit and this could have cause bugs where query history isn't updated accordingly
Time history regression Low Low This was moved around a bit and this could have cause bugs

How I tested performance

  1. Manually playing with search bar and suggestions. Used 6x slowdown, javascript profiler to find slow functions and excessive layout trashing and react dev tools profiler for finding redundant renders.
  2. Functional test that compared total React rendering time before vs after.

For my sample test results are: before ~150ms, after ~70ms.
I used react profiler API to sum up rendering time: https://reactjs.org/docs/profiler.html
And my sample test was typing the query using typeahead:

Sample test

  await queryBar.setQuery('extension.raw');
  await PageObjects.common.sleep(500);
  await testSubjects.click('autocompleteSuggestion-field-extension.raw-');
  await PageObjects.common.sleep(500);
  await testSubjects.click('autocompleteSuggestion-operator-:-');
  await PageObjects.common.sleep(500);
  await testSubjects.click('autocompleteSuggestion-value-jpg-');
  await PageObjects.common.sleep(500);
  await testSubjects.click('autocompleteSuggestion-conjunction-and-');
  await PageObjects.common.sleep(500);
  await queryBar.typeQuery('mach');
  await PageObjects.common.sleep(500);
  await testSubjects.click('autocompleteSuggestion-field-machine.os.raw-');
  await PageObjects.common.sleep(500);
  await testSubjects.click('autocompleteSuggestion-operator-:-');
  await PageObjects.common.sleep(500);
  await testSubjects.click('autocompleteSuggestion-value-osx-');
  await PageObjects.common.sleep(500);
  await queryBar.clickQuerySubmitButton();

@Dosant
Copy link
Contributor Author

Dosant commented Nov 23, 2021

@elasticmachine merge upstream

@Dosant Dosant force-pushed the d/2021-11-15-optimize-search-bar-react-rendering branch from 7944f9c to 4f0ee14 Compare December 6, 2021 16:57
@Dosant Dosant changed the title optimize search bar react rendering [SearchBar] Improve rendering performance Dec 7, 2021
@@ -286,7 +286,7 @@
"markdown-it": "^10.0.0",
"md5": "^2.1.0",
"mdast-util-to-hast": "10.0.1",
"memoize-one": "^5.0.0",
"memoize-one": "^6.0.0",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updating because now it has clear() method

import { shallowEqual } from '../../utils/shallow_equal';

const SuperDatePicker = React.memo(
EuiSuperDatePicker as any
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Couldn't find a way to avoid any here

);
});

function useDimensions(
Copy link
Contributor Author

@Dosant Dosant Dec 7, 2021

Choose a reason for hiding this comment

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

This is used for positioning suggestions component relative to the input field. Previously it was scattered between parent and child, now it is centralized inside a child.

This allowed to avoid trigger parent's re-rendering on page resizes and avoid reduce dimensions reading that cause layout trashing

@Dosant
Copy link
Contributor Author

Dosant commented Dec 7, 2021

@elasticmachine merge upstream

@Dosant Dosant added Feature:Query Bar Querying and query bar features release_note:skip Skip the PR/issue when compiling release notes Team:AppServicesUx v8.1.0 labels Dec 8, 2021
@Dosant Dosant marked this pull request as ready for review December 8, 2021 09:40
@Dosant Dosant requested a review from a team as a code owner December 8, 2021 09:40
@elasticmachine
Copy link
Contributor

Pinging @elastic/kibana-app-services (Team:AppServicesUx)

@Dosant
Copy link
Contributor Author

Dosant commented Dec 9, 2021

@elasticmachine merge upstream

kibanamachine and others added 3 commits December 9, 2021 06:08
…timize-search-bar-react-rendering

# Conflicts:
#	src/plugins/data/public/ui/query_string_input/query_string_input.tsx
…thub.com:Dosant/kibana into d/2021-11-15-optimize-search-bar-react-rendering
@jloleysens
Copy link
Contributor

@elasticmachine merge upstream

Copy link
Contributor

@jloleysens jloleysens left a comment

Choose a reason for hiding this comment

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

Great work @Dosant ! I tested locally and with react profiler I observed fewer peaks in re-renders as I was interacting with the search bar and suggestions 👏🏻

I left a few ideas on how we could improve some minor points.

}
}

if (document.activeElement !== null && document.activeElement.id === this.textareaId) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (document.activeElement !== null && document.activeElement.id === this.textareaId) {
if (document.activeElement?.id === this.textareaId) {

Copy link
Contributor

Choose a reason for hiding this comment

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

IMO this reads a lot easier, probably has a negligible performance overhead. I'll leave it up to you :)

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 agree that is easier to read, but logically this is a bit different. undefined === undefined case is different.
I'd leave it as is, because need to review this logical case first to make this change

window.addEventListener('scroll', handler, { passive: true, capture: true });

const resizeObserver =
typeof window.ResizeObserver !== 'undefined' &&
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can go without this check as it looks fairly supported in major browsers https://caniuse.com/resizeobserver.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agree that support is pretty good, but I was actually deciding between:

A. Using it only when available because this isn't critical functionality (so if it is not available, everything is still working, but with some position bugs)
B. Using resize-observer-polyfill like we do in other places. But I wanted to avoid including couple more Kbs into this async chunk for this - https://bundlephobia.com/package/resize-observer-polyfill@1.5.1
C. Using it directly assuming it always exists. I didn't go with it because it wasn't necessary to introduce this breaking change. As it might break someone who is using Kibana on some TV platform or older Safari without a good need for it. But since we are actually using it in some places directly, I created: #121509 to address this separately


return () => {
window.removeEventListener('scroll', handler, { capture: true });
if (resizeObserver) resizeObserver.disconnect();
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (resizeObserver) resizeObserver.disconnect();
resizeObserver?.disconnect();

Or just resizeObserver.disconnect(); if we remove the conditional

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is currently written so that resizeObserver is false | ResizeObserver so this doesn't work

@Dosant
Copy link
Contributor Author

Dosant commented Dec 17, 2021

@elasticmachine merge upstream

kibanamachine and others added 3 commits December 17, 2021 06:27
…timize-search-bar-react-rendering

# Conflicts:
#	src/plugins/data/public/ui/query_string_input/query_bar_top_row.tsx
#	src/plugins/data/public/ui/search_bar/search_bar.tsx
@Dosant
Copy link
Contributor Author

Dosant commented Dec 23, 2021

@elasticmachine merge upstream

@Dosant
Copy link
Contributor Author

Dosant commented Dec 30, 2021

@elasticmachine merge upstream

@Dosant
Copy link
Contributor Author

Dosant commented Jan 4, 2022

@elasticmachine merge upstream

Copy link
Member

@lukasolson lukasolson left a comment

Choose a reason for hiding this comment

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

Tested, functionality seems to be working properly.


if (
(newSelectionStart !== null && this.inputRef?.selectionStart !== newSelectionStart) ||
(newSelectionEnd !== null && this.inputRef?.selectionEnd !== newSelectionEnd)
Copy link
Member

Choose a reason for hiding this comment

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

When can newSelectionStart be null? And is this a case that should actually still be calling setState?

Copy link
Member

Choose a reason for hiding this comment

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

Also, should we add these same checks above for query?

Copy link
Contributor Author

@Dosant Dosant Jan 5, 2022

Choose a reason for hiding this comment

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

When can newSelectionStart be null? And is this a case that should actually still be calling setState?

Don't remember why I put it, I re-checeked and indeed doesn't seem necessary. Removed

Also, should we add these same checks above for query?

There is important check regarding the query inside onQueryStringChange

Just a note: I didn't try to put equal checks before setState everywhere, I was looking at render profile and added this checks why I saw it makes an impact

@Dosant
Copy link
Contributor Author

Dosant commented Jan 5, 2022

@elasticmachine merge upstream

@Dosant
Copy link
Contributor Author

Dosant commented Jan 6, 2022

@elasticmachine merge upstream

@kibana-ci
Copy link
Collaborator

💚 Build Succeeded

Metrics [docs]

Module Count

Fewer modules leads to a faster build time

id before after diff
data 508 517 +9

Public APIs missing comments

Total count of every public API that lacks a comment. Target amount is 0. Run node scripts/build_api_docs --plugin [yourplugin] --stats comments for more detailed information.

id before after diff
data 2943 2944 +1

Async chunks

Total size of all lazy-loaded chunks that will be downloaded as the user navigates the app

id before after diff
canvas 1010.9KB 1011.1KB +182.0B
data 101.5KB 108.3KB +6.8KB
dataViewEditor 115.1KB 115.3KB +182.0B
expressions 43.9KB 44.1KB +182.0B
lens 1020.9KB 1021.1KB +182.0B
ml 3.5MB 3.5MB +182.0B
timelines 225.0KB 225.2KB +182.0B
total +7.8KB

Page load bundle

Size of the bundles that are downloaded on every page load. Target size is below 100kb

id before after diff
data 444.8KB 445.0KB +165.0B
kbnUiSharedDeps-npmDll 6.1MB 6.1MB +336.0B
securitySolution 243.9KB 244.0KB +182.0B
total +683.0B
Unknown metric groups

API count

id before after diff
data 3340 3341 +1

History

To update your PR or re-run it, just comment with:
@elasticmachine merge upstream

@Dosant Dosant merged commit 00d1ad3 into elastic:main Jan 6, 2022
@kibanamachine kibanamachine added the backport:skip This commit does not require backporting label Jan 6, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
backport:skip This commit does not require backporting Feature:Query Bar Querying and query bar features release_note:skip Skip the PR/issue when compiling release notes v8.1.0
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants