1
\$\begingroup\$

I've got a big react app (with Redux) here that has a huge bottleneck.

We have implemented a product search by using product number or product name and this search is extremely laggy.

Problem: If a user types in some characters, those characters are shown in the InputField really retarded. The UI is frozen for a couple of seconds. In Internet Explorer 11, the search is almost unusable.

It's a Material UI TextField that filters products.

What I already did for optimization:

  1. Replaced things like style={{ maxHeight: 230, overflowY: 'scroll', }} with const cssStyle={..}
  2. Changed some critical components from React.Component to React.PureComponent
  3. Added shouldComponentUpdate for our SearchComponent
  4. Removed some unnecessary closure bindings
  5. Removed some unnecessary objects
  6. Removed all console.log()
  7. Added debouncing for the input field (that makes it even worse)

That's how our SearchComponent looks like at the moment:

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Downshift from 'downshift';

import TextField from '@material-ui/core/TextField';
import MenuItem from '@material-ui/core/MenuItem';
import Paper from '@material-ui/core/Paper';
import IconTooltip from '../helper/icon-tooltip';

import { translate } from '../../utils/translations';

const propTypes = {
  values: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
  legend: PropTypes.string,
  helpText: PropTypes.string,
  onFilter: PropTypes.func.isRequired,
  selected: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  isItemAvailable: PropTypes.func,
};

const defaultProps = {
  legend: '',
  helpText: '',
  selected: '',
  isItemAvailable: () => true,
};

const mapNullToDefault = selected =>
  (selected === null || selected === undefined ? '' : selected);

const mapDefaultToNull = selected => (!selected.length ? null : selected);

class AutoSuggestField extends Component {
  shouldComponentUpdate(nextProps) {
    return this.props.selected !== nextProps.selected;
  }

  getLegendNode() {
    const { legend, helpText } = this.props;
    return (
      <legend>
        {legend}{' '}
        {helpText && helpText.length > 0 ? (
          <IconTooltip helpText={helpText} />
        ) : (
          ''
        )}
      </legend>
    );
  }

  handleEvent(event) {
    const { onFilter } = this.props;

    const value = mapDefaultToNull(event.target.value);

    onFilter(value);
  }

  handleOnSelect(itemId, item) {
    const { onFilter } = this.props;
    if (item) {
      onFilter(item.label);
    }
  }

  render() {
    const { values, selected, isItemAvailable } = this.props;

    const inputValue = mapNullToDefault(selected);
    const paperCSSStyle = {
      maxHeight: 230,
      overflowY: 'scroll',
    };
    return (
      <div>
        <div>{this.getLegendNode()}</div>
        <Downshift
          inputValue={inputValue}
          onSelect={(itemId) => {
            const item = values.find(i => i.id === itemId);
            this.handleOnSelect(itemId, item);
          }}
        >
          {/* See children-function on https://github.com/downshift-js/downshift#children-function */}
          {({
            isOpen,
            openMenu,
            highlightedIndex,
            getInputProps,
            getMenuProps,
            getItemProps,
            ref,
          }) => (
            <div>
              <TextField
                className="searchFormInputField"
                InputProps={{
                  inputRef: ref,
                  ...getInputProps({
                    onFocus: () => openMenu(),
                    onChange: (event) => {
                      this.handleEvent(event);
                    },
                  }),
                }}
                fullWidth
                value={inputValue}
                placeholder={translate('filter.autosuggest.default')}
              />
              <div {...getMenuProps()}>
                {isOpen && values && values.length ? (
                  <React.Fragment>
                    <Paper style={paperCSSStyle}>
                      {values.map((suggestion, index) => {
                        const isHighlighted = highlightedIndex === index;
                        const isSelected = false;
                        return (
                          <MenuItem
                            {...getItemProps({ item: suggestion.id })}
                            key={suggestion.id}
                            selected={isSelected}
                            title={suggestion.label}
                            component="div"
                            disabled={!isItemAvailable(suggestion)}
                            style={{
                              fontWeight: isHighlighted ? 800 : 400,
                            }}
                          >
                            {suggestion.label}
                          </MenuItem>
                        );
                      })}
                    </Paper>
                  </React.Fragment>
                ) : (
                  ''
                )}
              </div>
            </div>
          )}
        </Downshift>
      </div>
    );
  }
}

AutoSuggestField.propTypes = propTypes;
AutoSuggestField.defaultProps = defaultProps;

export default AutoSuggestField;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.5.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.5.0/umd/react-dom.production.min.js"></script>

EDIT (added some infos here:)

  • Downshift is this package here: https://www.npmjs.com/package/downshift
  • I need to lift the value to the store as it is needed in other components as well
  • Will change it back to React.Component and use some deep compare then, thx
  • throttle sounds well, but I am not doing any API request in the search
  • No API requests. The search only works with 'local' stuff (Redux, local Arrays, local JSON-objects).

Here is some more Code, that calls our AutoSuggestField:

import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import { removeFilter, setFilter } from '../store/filter-values/actions';
import FilterSelection from './components/FilterSelection';
import withFilterService from './service/withFilterService';

import AutoSuggestField from '../components/filter/AutoSuggestField';
import FilterService from './service/FilterService';
import { ProductCategory } from '../data/model/category';

export class TrieSearchFilterSelection extends FilterSelection {
  constructor(attributeName, selected, trieResult) {
    super(attributeName, selected);
    this.filterResultIds = {};
    trieResult.forEach((product) => {
      this.filterResultIds[product.id] = true;
    });
  }

  // eslint-disable-next-line class-methods-use-this
  compareSpec() {
    throw Error('Not implemented');
  }

  filter(product) {
    if (this.isReset()) {
      return true; // if this filter is reset(ed) don't do anything
    }

    return !!this.filterResultIds[product.id];
  }
}

const propTypes = {
  attributeName: PropTypes.string.isRequired,
  selected: PropTypes.instanceOf(TrieSearchFilterSelection),
  ignoreOwnAttributeForAvailability: PropTypes.bool,
  onRemoveFilter: PropTypes.func.isRequired,
  onSetFilter: PropTypes.func.isRequired,
  filterService: PropTypes.instanceOf(FilterService).isRequired,
  category: PropTypes.instanceOf(ProductCategory).isRequired,
};

const defaultProps = {
  selected: null,
  ignoreOwnAttributeForAvailability: true,
};

class CameraTitleOrdernumberFilter extends PureComponent {
  constructor(props) {
    super(props);
    const { filterService, category } = this.props;

    this.trieSearch = filterService
      .getServiceForCategory(category.name)
      .getTrieSearch();
  }

  getValues() {
    const { selected, attributeName } = this.props;

    const products = this.trieSearch.getSearchResultOrAllProducts(selected ? selected.getSelectedValue() : '');
    const availabitlity = this.trieSearch.getProductAvailability(attributeName);

    return products.map((product) => {
      const { name } = product;
      const ordernumber = product.specs.specMap.ordernumber.value;

      return {
        label: `${product.name} - ${ordernumber}`,
        name,
        ordernumber,
        id: product.id,
        available: availabitlity.includes(product.id),
      };
    });
  }

  handleFilterSelected(newValue) {
    const { attributeName, onRemoveFilter, onSetFilter } = this.props;

    let products = [];
    if (newValue) {
      products = this.trieSearch.get(newValue);
    }

    const selectionObj = new TrieSearchFilterSelection(
      attributeName,
      newValue || null,
      products
    );

    if (selectionObj.isReset()) {
      onRemoveFilter();
    } else {
      onSetFilter(selectionObj);
    }
  }

  render() {
    const { selected } = this.props;
    const valuesToUse = this.getValues();
    const selectedToUse =
      !!selected && 'selected' in selected ? selected.selected : null;

    return (
      <AutoSuggestField
        {...this.props}
        values={valuesToUse}
        selected={selectedToUse}
        isItemAvailable={item => item.available}
        onFilter={newValue => this.handleFilterSelected(newValue)}
      />
    );
  }
}

CameraTitleOrdernumberFilter.propTypes = propTypes;
CameraTitleOrdernumberFilter.defaultProps = defaultProps;

export {
  CameraTitleOrdernumberFilter as UnconnectedCameraTitleOrdernumberFilter,
};

const stateMapper = (
  { filterValues },
  { category, attributeName, selected }
) => ({
  selected:
    (category.name in filterValues &&
      attributeName in filterValues[category.name] &&
      filterValues[category.name][attributeName]) ||
    selected,
});

const mapDispatchToProps = (dispatch, { category, attributeName }) => ({
  onRemoveFilter: () => dispatch(removeFilter(category.name, attributeName)),
  onSetFilter: (newValue) => {
    dispatch(setFilter(category.name, attributeName, newValue));
  },
});

export const doConnect = component =>
  connect(
    stateMapper,
    mapDispatchToProps
  )(withFilterService(component));

export default doConnect(CameraTitleOrdernumberFilter);

It seems, that I did not find the performance problem as it still exists. Can someone help here?

\$\endgroup\$
1
\$\begingroup\$

Difficult to fix this kind of thing without being able to run the code. That said, having a look at your comments above, and what you've got there, I would suggest a few things.

First off, when things start to lag, the UI is freezing up, or you're r experiencing render thrashing that can also make things lag, usually I would start with:

  • It looks like you're seting the value from the store, as you type, which is an unneccessary bit of heavy lifting. Use local state and move away from controlled components for form fields.
  • Change PureComponent for Component and use ShouldComponentUpdate along with a deep equality check (lodash, underscore, both have _.equal or roll your own). PureComponent only does a shallow comparison, so you may still see all sorts of thrashing in your react dev tools. I've seen this often on lists, and list Items.
  • use throttle rather than debounce (debounce takes the last event after a given time, where throttle will take it at a set interval but avoid hammering your API on every request) - again assuming your onFilter function triggers an API call.
  • Maybe you're doing do much at once
    • if the above is triggering multiple API calls, are you cancelling, and then calling again?
    • Are you using selectors to filter the list client side, to ensure you're not triggering additional renders unneccessarily?
    • marshal the events in your application, so that you show a loading indicator, and then only filter your list once the results are available.
  • Look at the libs/deps
    • I'm not experienced with Downshift, though it may be interacting with the DOM directly, try removing this.
    • I've had no issues with the material-ui inputs, so I wouldn't look there
    • have you got anything else going on at the same time? Logging in redux, bugsnag or similar, rendering images?, animations? If you watch the react tree in dev tools is anything else rendering unnecessarily? Try disabling these one by one if you can, to find the culprit.
    • Something I have had issues with is the material-ui spinner, if you're using that anywhere in the same view, try removing that as well.
  • Look outside the scope of your current component

Thats a really hard one without being able to fiddle with the code, Hope this is helpful.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thank you very much. I edited my post above (added some code as well). Can you have a look at the other code ? \$\endgroup\$ – Gutelaunetyp Jun 30 at 20:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.