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:
- Replaced things like style={{ maxHeight: 230, overflowY: 'scroll', }} with const cssStyle={..}
- Changed some critical components from React.Component to React.PureComponent
- Added shouldComponentUpdate for our SearchComponent
- Removed some unnecessary closure bindings
- Removed some unnecessary objects
- Removed all console.log()
- 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?