英文:
React (with Ant Design table) is **really** slow: how to optimize?
问题
我创建了一个使用 Ant Design 表格组件的 React 小型调度系统。我的问题是,当我们添加大约60行(听起来不是很多,对吧?)时,系统变得非常慢(即使水平滚动也会产生一些小的延迟)。像我做的任何操作可能需要大约5秒钟:
你有任何关于我做错了什么,以及如何调试这些问题的想法吗?
编辑 我还尝试了添加 memo
和 useCallback
,但问题仍然存在...
示例代码:
<!doctype html>
<html lang="en">
<head>
<!-- 省略了一些内容 -->
</head>
<body>
<div id="planning"></div>
<script type="text/babel">
// 省略了一些内容
</script>
</body>
</html>
如果你需要更多的帮助,请告诉我。
英文:
I created a mini-scheduling system in react using Ant Design table components. My issue is that when we add around 60 lines (not that crazy right?), the system is really slow (even scrolling horizontally produces a small lag). Like any operation I do takes maybe 5 seconds:
Any idea what I’m doing wrong, and how we can debug these issues?
EDIT I also tried to ad memo
and useCallback
, but same issue…
Example code:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Planning</title>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.development.js" crossorigin></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.development.js" crossorigin></script>
<script src="https://cdn.jsdelivr.net/npm/react-redux@8.0.5/dist/react-redux.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@reduxjs/toolkit@1.9.3/dist/redux-toolkit.umd.js"></script>
<script src=" https://cdn.jsdelivr.net/npm/dayjs@1.11.7/dayjs.min.js "></script>
<script src=" https://cdn.jsdelivr.net/npm/antd@5.5.0/dist/antd.js "></script>
<link href=" https://cdn.jsdelivr.net/npm/antd@5.5.0/dist/reset.min.css " rel="stylesheet">
</head>
<body>
<div id="planning"></div>
<script type="text/babel">
const { useContext, useEffect, useRef, useState, memo, useCallback } = React;
const { useDispatch, useSelector, Provider } = ReactRedux;
const { configureStore, createSlice } = RTK;
const { Button, Form, Input, InputNumber, Popconfirm, Table, Select, Tag, Slider } = antd;
// ***************************
// Store
// ***************************
const stateToSaveReducer = createSlice({
name: "stateToSave",
initialState: {
// We try to keep everything in a single table, to provide simple copy/paste and visualisation etc...
allSchedules: [
{
columns: [
// subsetOf are list separated with ;, this allows easy import/export to csv/excel/…
{field: "Place", kind: "subsetOf", description: "Endroit(s) où la personne travaille.", values: ["Place 1", "Place 2"]},
{field: "Type", kind: "oneOf", description: "Type de travail, soit aides soignantes, soit agents service logistique", values: ["AS", "ASL"]},
{field: "Pourcentage", kind: "intBetween", values: [0, 100], description: "Pourcentage travail.", marks: [0, 50, 80, 100]},
],
allowedTags: [
{name: "Place 1"},
{name: "Place 2"},
{name: "Morning"},
{name: "Lunch"},
{name: "Meeting 1"},
{name: "Meeting 2"},
{name: "Evening"},
],
nbDays: 3*7,
scheduleAndEmployee: [
{
name: "Alice Montest",
Place: "Place 1;Place 2",
Type: "AS",
Pourcentage: "80",
days: [
// Just a list of tags, it will be turned later into time etc.
// for now these tags have no meaning.
"Lunch;Meeting 1;Morning;Place1",
"Evening",
],
},
{
name: "Bob Monnomdefam",
Place: "MAPPA",
Type: "AS",
Pourcentage: "100",
days: [
// Just a list of tags, it will be turned later into time etc.
// for now these tags have no meaning.
"Meeting 2;Morning;Place 1",
"Evening",
"Lunch"
],
}
]
}
]
},
reducers:{
removeEmployee: (state, action) => {
state.allSchedules[action.payload.idSchedule].scheduleAndEmployee = state.allSchedules[action.payload.idSchedule].scheduleAndEmployee.filter((e,i) => i != action.payload.key)
},
addEmployee: (state, action) => {
var t = state.allSchedules[action.payload.idSchedule].scheduleAndEmployee;
t.push({
name: "Employé " + (t.length+1)
});
},
// Duplicate the last employee, except for name, of course, and days
duplicateLastEmployee: (state, action) => {
var t = state.allSchedules[action.payload.idSchedule].scheduleAndEmployee;
const {name, days, ...rest} = t[t.length-1];
t.push({...rest,
name: "Employé " + (t.length+1)
});
},
updateEmployee: (state, action) => {
var t = state.allSchedules[action.payload.idSchedule].scheduleAndEmployee;
const {key, ...rest} = action.payload.newEmployee;
state.allSchedules[action.payload.idSchedule].scheduleAndEmployee[key] = rest;
},
updateEmployeeDay: (state, action) => {
var t = state.allSchedules[action.payload.idSchedule].scheduleAndEmployee;
if (!state.allSchedules[action.payload.idSchedule].scheduleAndEmployee[action.payload.key].hasOwnProperty("days")) {
state.allSchedules[action.payload.idSchedule].scheduleAndEmployee[action.payload.key].days = [];
}
state.allSchedules[action.payload.idSchedule].scheduleAndEmployee[action.payload.key].days[action.payload.day] = action.payload.dayContent;
}
}
})
const store = configureStore({
reducer: {
stateToSave: stateToSaveReducer.reducer
},
})
const { removeEmployee, addEmployee, duplicateLastEmployee, updateEmployee, updateEmployeeDay } = stateToSaveReducer.actions
const selectorScheduleAndEmployee = (idSchedule, state) => {
return state.stateToSave.allSchedules[idSchedule].scheduleAndEmployee.map((x, i) => {return {...x, key:i}})
}
// ***************************
// Main grid
// ***************************
const AddEmployeeButton = ({...props}) => {
const dispatch = useDispatch();
const [nbEmployee, setNbEmployee] = useState(3);
const handleAdd = useCallback((e) => {
Array(nbEmployee).fill().map(() => dispatch(duplicateLastEmployee({idSchedule: props.idSchedule})))
},[nbEmployee, props.idSchedule]);
const handleClick = useCallback(e => {e.stopPropagation();e.preventDefault()}, []);
return <Button onClick={handleAdd} type="primary" style={{ marginBottom: 16 }}>
Ajouter <InputNumber min={1} value={nbEmployee} onChange={setNbEmployee} onClick={handleClick} size="small" /> employé(e)
</Button>
};
const ComponentIntBetween = memo(({oldRow, nameField, values, marks, idSchedule, ...props}) => {
const dispatch = useDispatch();
const valueSelect = useSelector((state) =>
parseInt(state.stateToSave.allSchedules[idSchedule].scheduleAndEmployee[oldRow.key][nameField]));
const handleChange = (newValue) => {
const newEmployee = {...oldRow, [nameField]: newValue.toString()};
dispatch(updateEmployee({idSchedule: idSchedule, newEmployee: newEmployee}));
};
return <Slider
min={values[0]}
max={values[1]}
marks={Object.fromEntries(marks.map((x) => {return [x, x]}))}
style={{ width: "80%" }}
onChange={handleChange}
value={typeof valueSelect === 'number' ? valueSelect : 0}
/>
});
const ComponentSelectFromList = memo(({oldRow, nameField, values, idSchedule, ...props}) => {
const dispatch = useDispatch();
const valueSelect = useSelector((state) => state.stateToSave.allSchedules[idSchedule].scheduleAndEmployee[oldRow.key][nameField]);
const handleChange = (newValue) => {
const newEmployee = {...oldRow, [nameField]: newValue};
dispatch(updateEmployee({idSchedule: idSchedule, newEmployee: newEmployee}));
};
return <Select
defaultValue={values[0]}
value={valueSelect}
style={{ width: "100%" }}
bordered={false}
onChange={handleChange}
options={values.map((x) => {return {value: x, label: x}})}
/>
});
const tagRender = memo((props) => {
const { label, value, closable, onClose } = props;
const onPreventMouseDown = useCallback((event) => {
event.preventDefault();
event.stopPropagation();
},[]);
return (
<Tag
onMouseDown={onPreventMouseDown}
closable={closable}
onClose={onClose}
style={{
marginRight: 3,
}}
>
{label}
</Tag>
);
});
const ComponentSelectSubsetFromList = memo(({oldRow, nameField, values, idSchedule, ...props}) => {
const dispatch = useDispatch();
const valueSelect = useSelector((state) => state.stateToSave.allSchedules[idSchedule].scheduleAndEmployee[oldRow.key][nameField].split(";").filter(element => element));
const handleChange = (newValue) => {
const newEmployee = {...oldRow, [nameField]: newValue.join(";")};
dispatch(updateEmployee({idSchedule: idSchedule, newEmployee: newEmployee}));
};
return <Select
mode="multiple"
showArrow
tagRender={tagRender}
value={valueSelect}
style={{ width: "100%" }}
bordered={false}
onChange={handleChange}
options={values.map((x) => {return {value: x}})}
/>
});
const ComponentDay = memo(({oldRow, day, idSchedule, ...props}) => {
const dispatch = useDispatch();
const valueSelect = useSelector((state) => {
if (!state.stateToSave.allSchedules[idSchedule].scheduleAndEmployee[oldRow.key].hasOwnProperty("days")) {
return []
}
if (state.stateToSave.allSchedules[idSchedule].scheduleAndEmployee[oldRow.key].days.hasOwnProperty(day)) {
return state.stateToSave.allSchedules[idSchedule].scheduleAndEmployee[oldRow.key].days[day].split(";").filter(element => element)
} else {
return []
}});
const allowedTags = useSelector((state) =>
state.stateToSave.allSchedules[idSchedule].allowedTags);
const handleChange = (newValue) => {
dispatch(updateEmployeeDay({idSchedule: idSchedule, key: oldRow.key, day: day, dayContent: newValue.join(";")}));
};
return <Select
mode="multiple"
showArrow
tagRender={tagRender}
value={valueSelect}
style={{ width: "100%" }}
bordered={false}
onChange={handleChange}
options={allowedTags.map((x) => {return {value: x.name}})}
/>
});
const EditableContext = React.createContext(null);
const EditableRow = memo(({ index, ...props }) => {
const [form] = Form.useForm();
return (
<Form form={form} component={false}>
<EditableContext.Provider value={form}>
<tr {...props} />
</EditableContext.Provider>
</Form>
);
});
const EditableCell = memo(({
title,
editable,
children,
dataIndex,
record,
handleSave,
...restProps
}) => {
const [editing, setEditing] = useState(false);
const inputRef = useRef(null);
const form = useContext(EditableContext);
useEffect(() => {
if (editing) {
inputRef.current.focus();
}
}, [editing]);
const toggleEdit = useCallback(() => {
setEditing(!editing);
form.setFieldsValue({
[dataIndex]: record[dataIndex],
});
},[editing,dataIndex,record]);
const save = async () => {
try {
const values = await form.validateFields();
toggleEdit();
handleSave({
...record,
...values,
});
} catch (errInfo) {
console.log('Save failed:', errInfo);
}
};
let childNode = children;
if (editable) {
childNode = editing ? (
<Form.Item
style={{
margin: 0,
}}
name={dataIndex}
rules={[
{
required: true,
message: `${title} is required.`,
},
]}
>
<Input ref={inputRef} onPressEnter={save} onBlur={save} />
</Form.Item>
) : (
<div
className="editable-cell-value-wrap"
style={{
paddingRight: 24,
}}
onClick={toggleEdit}
>
{children}
</div>
);
}
return <td {...restProps}>{childNode}</td>;
});
// Main table
const Antgrid = () => {
const idSchedule = 0;
const dataSource = useSelector((state) => selectorScheduleAndEmployee(idSchedule, state));
const customColumns = useSelector((state) => state.stateToSave.allSchedules[idSchedule].columns);
const nbDays = useSelector((state) => state.stateToSave.allSchedules[idSchedule].nbDays);
const dispatch = useDispatch()
const defaultColumns = [
{
title: 'Nom',
dataIndex: 'name',
fixed: 'left',
width: 180,
editable: true
}].concat(customColumns.map((c) => {
// We add the columns like percent of work etc…
// Depending on the type of the column, we display different components
var col = {
title: c.field,
dataIndex: c.field,
width: 180,
};
switch (c.kind) {
case "oneOf":
return {...col,
render: (_, record) => {
return <ComponentSelectFromList nameField={c.field} idSchedule={idSchedule} oldRow={record} values={c.values} />
}
}
break;
case "subsetOf":
return {...col,
render: (_, record) => {
return <ComponentSelectSubsetFromList nameField={c.field} idSchedule={idSchedule} oldRow={record} values={c.values} />
}
}
break;
case "intBetween":
return {...col,
render: (_, record) => {
return <ComponentIntBetween nameField={c.field} idSchedule={idSchedule} oldRow={record} values={c.values} marks={c.marks ? c.marks : []}/>
}
}
break;
default:
return col
}
})).concat(Array.from({length: nbDays}, (x,i) => {
// We add the actual working days columns
return {
title: 'Day',
dataIndex: ['days', i],
width: 150,
render: (_, record) => {
return <ComponentDay day={i} idSchedule={idSchedule} oldRow={record} />
}
}
})).concat([{
title: 'Operations',
dataIndex: 'Operations',
width: 100,
render: (_, record) => (
dataSource.length >= 1 ? (
<Popconfirm title="Sure to delete?" onConfirm={() => dispatch(removeEmployee({idSchedule: idSchedule, key: record.key}))}>
<a>Delete</a>
</Popconfirm>
) : null),
}]);
const handleSave = (row) => {
dispatch(updateEmployee({idSchedule: idSchedule, newEmployee: row}))
};
// We override defaults table elements to make them editable
const components = {
body: {
row: EditableRow,
cell: EditableCell,
},
};
const columns = defaultColumns.map((col) => {
if (!col.editable) {
return col;
}
return {
...col,
onCell: (record) => ({
record,
editable: col.editable,
dataIndex: col.dataIndex,
title: col.title,
handleSave,
}),
};
});
// const columns = defaultColumns;
return (
<div>
<Table
size="small"
dataSource={dataSource}
columns={columns}
sticky
pagination={false}
scroll={{ x: "100%" }}
components={components}
/>
<AddEmployeeButton idSchedule={idSchedule}/>
</div>
);
};
function App() {
return (<>
<h1>Planning</h1>
<Antgrid/>
</>)
};
const container = document.getElementById('planning');
const root = ReactDOM.createRoot(container);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
)
</script>
</body>
</html>
答案1
得分: 1
memo()
应用于所有单元格useCallback
应用于所有回调函数useMemo()
应用于所有可能更改的样式和对象- 非常重要(仅此更改可能足够?):像这样设置
shouldCellUpdate
:
render: (_, record) => {
return <EditableCell nameField={"name"} idSchedule={idSchedule} oldRow={record} />
},
// https://github.com/ant-design/ant-design/issues/23763
shouldCellUpdate: (record, prevRecord) => !shallowEqual(record, prevRecord)
仍然存在一些滞后,但可能只有半秒,所以我可以接受。我还将第一个可编辑文本区域更改为另一个单元格区域,这样我就不需要担心是否存在 shouldCellUpdate
用于此自定义行。
英文:
Ok, I found a solution to the refresh problem (but not to the long creation time… but that time is only once so I can live with it for now). The idea is to combine:
memo()
on all cellsuseCallback
on all functions that are callbacksuseMemo()
on all styles and objects that might change- and very importantly (this only change might even be enough?): set
shouldCellUpdate
like in:
render: (_, record) => {
return <EditableCell nameField={"name"} idSchedule={idSchedule} oldRow={record} />
},
// https://github.com/ant-design/ant-design/issues/23763
shouldCellUpdate: (record, prevRecord) => !shallowEqual(record, prevRecord)
there is still a bit of lag, but it’s maybe half a second so I can live with that. I also changed the first editable text zone into another cell zone, this way I don’t need to worry if shouldCellUpdate
exists for this custom row.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论