最近写表格相关需求的时候,发现点击一行的checkbox后,卡顿很久才会选上。于是对表格进行了优化。这种数据偏多导致交互时卡顿的情况很常见,主要是组件渲染的问题,所以研究下怎么从渲染方面优化应用性能。
经过研究发现主要原因在于:每当点击一个checkbox时,Table内所有组件,如TableRow、TableHeader、TablePagination全部重新渲染了一遍!造成了严重的卡顿。所以我们要做的就是减少无用的渲染。
最理想的情况是:当UI变化时,只需要真正变化的那个组件重渲染,其他组件一律不渲染。
方法
1.不要在render方法中定义组件
// 以下为错误示范,万不可取!class Table extends Component {render() {const TableRow = (data) => <tr>...</tr>const rows = this.props.data.map(item => <TableRow data={item} />)return (<TableHeader />{rows})}}// 正确的操作const TableRow = (data) => <tr>...</tr>class Table extends Component {render() {return (<TableHeader />{this.props.data.map(item => <TableRow data={item} />)})}}
原因:若TableRow组件定义在Table的render方法里,只要Table的props或state一旦修改,导致Table执行render函数,进一步导致TableRow和rows都是新的、全部的TableRow全部重渲染
解决方式:
- 单独定义组件
- render里不要定义多余的变量,如rows,尽可能直接写在return的jsx里
2.使用PureComponent、memo
React.PureComponent作用是只要组件的props、state不变,父组件更新不会让组件随之更新,适用于class组件。
React.memo作用同理,适用于函数式组件。
// class组件class TableRow extends React.PureComponent {render() {return (...)}}// 函数式组件const TableRow = React.memo(() => {return (...)})
例子:本案例中使用了Material UI v4.9,其对组件的封装并没有使用PureComponent,导致TableRow组件重渲染时,每个单元格也重渲染。
解决方法:在组件外包一层memo控制渲染。
// 以下为错误示范class TableRow extends React.PureComponent {render() {return (<TableCell><a href=''>{name}</a></TableCell><TableCell>{gender}</TableCell><TableCell>{age}</TableCell>)}}// 正确的操作const TableCell = React.memo((props) => <MuiTableCell ...props />)class TableRow extends React.PureComponent {render() {return (<TableCell><a href=''>{name}</a></TableCell><TableCell>{gender}</TableCell><TableCell>{age}</TableCell>)}}
3.尽可能传递基础类型数据、不变的数据
若传递的props是引用类型,则props若内容不变,引用变了,则导致组件无意义的渲染。
例子一:比如Table用一个数组 selectedRows存储当前选择的row,然后将selectedRows传给每一行TableRow,用于判断该TableRow是否被选择。每次选择后,希望只有新选择的row重渲染,而实际上使用setState更新state后,selectedRows为新数组,导致全部row都重新渲染。
解决方法:用布尔值isSelected代替,在TableRow组件外判断是否被选择。这样在不断切换选择row时,传递的参数一直是false,不会触发渲染。
// 以下为错误示范,万不可取!class Table extends Component {render() {return ({this.props.data.map(item =><TableRow data={item} selectedRows={selectedRows} />)})}}// 正确的操作class Table extends Component {render() {return ({this.props.data.map(item => {const isSelected = selectedRows.includes(item.id)return <TableRow data={item} isSelected={isSelected} />})})}}
例子二:在props很多时,有人会喜欢把同一类的props组成一个对象,传给子组件(如下)。但这样带来的问题就是每次Table重渲染时,{page,count: dataCount,rowsPerPage: rowsPerPage}会组成新对象,会导致Pagination也重渲染
解决方式:props拆开传递,传递基本类型数据
// 以下为错误示范,万不可取!class Table extends Component {render() {return (...<TablePaginationpagination={{page,count: dataCount,rowsPerPage: rowsPerPage}}/>)}}// 正确的操作class Table extends Component {render() {return (...<TablePaginationpage={page}count={dataCount}rowsPerPage={rowsPerPage}/>)}}
4.组件分割
组件的子组件也属于组件props一部分,当是string等基础类型时,不会导致重渲染;若是node类型,就算数据没有变化,也会产生新node重渲染。
解决方法:分割成小组件,memo包上。
// 以下为错误示范class TableRow extends React.PureComponent {render() {return (<TableCell><a href=''>{name}</a></TableCell><TableCell>{gender}</TableCell><TableCell>{age}</TableCell>)}}// 正确的操作const TableCell = React.memo((props) => <MuiTableCell ...props />)const NameTabelCell = React.memo(({ name }) =><TableCell><a href=''>{name}</a></TableCell>)class TableRow extends React.PureComponent {render() {return (<NameTabelCell name={name} /><TableCell>{gender}</TableCell><TableCell>{age}</TableCell>)}}
总结
引起组件重渲染的原因
- 父组件传递props变化
- 组件内部state变化
- 父组件重渲染
- 组件在父组件内重新生成了
