阻止列表选中元素时整体重渲染的React性能优化问题
React列表性能优化方案
问题
选中列表项时,仅需新选中项和前选中项重渲染,但当前数百条规模的列表会整体重渲染,导致页面卡顿。已尝试React.memo但未达到预期优化效果。
原因分析
- 使用index作为列表项key:index作为key无法让React精准识别列表项的唯一身份,且在列表结构变化时会引发不必要的组件卸载/重建;
- 动态生成style对象传递给子组件:在
TermineList的map循环中,每次渲染都会创建新的style对象,React.memo的浅比较会判定props发生变化,导致所有子组件强制重渲染; - 样式逻辑集中在父组件:父组件控制所有子组件的样式,导致每个子组件都依赖全局的
currentlySelected状态,只要该状态变化,所有子组件的props都会被重新计算。
优化方案
- 使用数据项唯一ID作为key:用
termin.id替代index,让React能精准跟踪列表项的身份变化; - 将样式逻辑移至子组件内部:子组件根据自身数据ID和全局选中状态自行计算样式,避免父组件传递动态生成的style对象;
- 确保props稳定性:利用
useState返回的set函数的稳定性,以及不可变的数据引用,让React.memo的浅比较逻辑正确生效。
修改后的代码示例
父组件(SubSiteDokumentation)
原有逻辑无需修改:
export default function SubSiteDokumentation(props: any) { const StammdatenContext = React.useContext(ProbandContext); const [selectedId, setSelectedId] = React.useState(""); const { formData, handleGetDetail, isLoading, isError } = useAPIEndpoint({ endpoint: "KontaktetermineList", formName: "Termine", pk: StammdatenContext.currentPnr, enableSnackbar: true, initialLoading: true, }); React.useEffect(() => { handleGetDetail(); }, []); if (isError) { return ( <Typography variant="h4" align="center" color="red"> Termine konnten nicht geladen werden! Versuchen sie es später erneut oder wenden sie sich an den Administrator </Typography> ); } if (isLoading) { return <Spinner animation="border"></Spinner>; } return ( <Grid container spacing={2}> <Grid item xs={7}> <TermineList data={formData} changeSelected={setSelectedId} currentlySelected={selectedId} ></TermineList> </Grid> {/* <Grid item xs={5}> <TerminDetail currentlySelected={selectedId}></TerminDetail> </Grid> */} </Grid> ); }
列表组件(TermineList)
替换key为termin.id,移除style传递,改为传递全局选中状态和索引:
function TermineList(props: any) { return ( <Box pl={"10px"} sx={{ background: "#b4f5bf", }} className="my-sidebarAuftraege my-nice-scrollbarBox" > <ListSubheader sx={{ background: "#b4f5bf", zIndex: "1000" }}> <Typography align="center" variant="h5" color="black"> Dokumentation </Typography> </ListSubheader> <List component="div" disablePadding sx={{ background: "#e3ffe9", borderRadius: "10px" }} > {props.data.map((termin: any, index: number) => { return ( <React.Fragment key={termin.id}> <TermineListEntry termin={termin} changeSelected={props.changeSelected} currentlySelected={props.currentlySelected} index={index} /> <Divider sx={{ borderBottomWidth: 2, backgroundColor: "black" }} ></Divider> </React.Fragment> ); })} <ListItem sx={{ display: "flex", justifyContent: "center", backgroundColor: (props.data.length % 2 === 1 && "#00000011") || "#00000000", }} > {/* <Box sx={{ display: "flex", justifyContent: "center" }}> <div>test</div> </Box> */} </ListItem> </List> </Box> ); }
列表项组件(TermineListEntry)
内部计算样式,保留React.memo:
const TermineListEntry = React.memo((props: any) => { // 子组件内部计算样式,避免父组件传递新对象 const style = { backgroundColor: props.index % 2 === 1 ? "#00000011" : "", borderColor: "#89ca07", borderBottom: props.currentlySelected === props.termin.id ? "dashed" : "", }; return ( <ListItemButton style={style} onClick={() => { props.changeSelected(props.termin.id); }} > <ListItemText> <Grid container> <Grid item xs={2}> {Moment(props.termin.datum).format("DD.MM.YYYY")} </Grid> <Grid item xs={2}> {props.termin.uhrzeit} </Grid> <Grid item xs={6}> {props.termin.inhalt} </Grid> <Grid item xs={1}> {props.termin.art} </Grid> <Grid item xs={1}> {props.termin.kv} </Grid> </Grid> </ListItemText> </ListItemButton> ); });
额外优化建议
- 若列表数据量超过千条,可引入虚拟滚动(如
react-window或Material-UI的VirtualizedList),仅渲染可视区域内的列表项,大幅降低渲染压力; - 确保
formData为不可变数据,避免数据引用变化触发父组件不必要的重渲染。
内容的提问来源于stack exchange,提问作者Zergoholic
相关产品推荐
相关产品推荐

