最近写表格相关需求的时候,发现点击一行的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 (
...
<TablePagination
pagination={{
page,
count: dataCount,
rowsPerPage: rowsPerPage
}}
/>
)
}
}
// 正确的操作
class Table extends Component {
render() {
return (
...
<TablePagination
page={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变化
- 父组件重渲染
- 组件在父组件内重新生成了