随着组件数量的增加,应对性能问题



原来问题出在我的电脑上。然而,James提出了一些关于如何隔离问题以及利用useCallback和useMemo进行优化的好观点

我的react应用程序的性能出现问题。现在我排除了代码,因为我觉得可能有一些常识性的答案。

这是演示视频

这里有一些指针

  • 我没有不必要的重新渲染。悬停时仅渲染单个组件
  • 动画被限制在悬停元素的容器div中,因此悬停时不会在容器外的页面上重新绘制
  • 我没有使用任何沉重的代码悬停效果或检测

我想知道还有什么原因会导致这样的性能问题。据我所知,如果组件只是放在那里,而不是重新绘制,那么它们的数量应该无关紧要。

这是正在设置动画的卡片组件的代码。我不太确定在这里展示什么是重要的。显示所有卡片的父组件不会重新渲染。

export default function CardFile(props) {
// Input field
const input = useRef(null)
//Input state
const [inputActive, setInputActive] = useState(false);
const [title, setTitle] = useState(props.file.name)
const [menuActive, setMenuActive] = useState(false)
const [draggable, setDraggable] = useState(true)
const [isDragged, setIsDragged] = useState(false)
// counter > 0 = is hovered
const [dragCounter, setDragCounter] = useState(0)


//_________________ FUNCTIONS _________________//

// Handle file delete
const handleDelete = (e) => {
firebase.firestore().collection('users').doc(props.file.owner).collection('files').doc(props.file.id).delete().then(() => {
console.info('Deleted')
}).catch((err) => console.err(err))
}
// Prevent default if necessary
const preventDefault = (e) => {
e.preventDefault()
e.stopPropagation()
}
// Handle rename
const handleRename = (e) => {
e.stopPropagation()
setMenuActive(false)
setInputActive(true)
}
// Handle change
const handleChange = () => {
setTitle(input.current.value)
}
// Handle focus loss
const handleFocusLoss = (e) => {
e.stopPropagation()
setInputActive(false)
firebase.firestore().collection('users').doc(props.file.owner).collection('files').doc(props.file.id).update({ name: title })
.then(() => {
console.info('Updated title')
}).catch((err) => console.error(err))
}
// Handle title submit
const handleKeyPress = (e) => {
console.log('key')
if (e.code === "Enter") {
e.preventDefault();
setInputActive(false)
firebase.firestore().collection('users').doc(props.file.owner).collection('files').doc(props.file.id).update({ name: title })
.then(() => {
console.info('Submitted title')
}).catch((err) => console.error(err))
}
}
// Set input focus
useEffect(() => {
if (inputActive) {
input.current.focus()
input.current.select()
}
}, [inputActive])

//_____________________________DRAGGING___________________________//
//Handle drag start
const onDragStartFunctions = () => {
props.onDragStart(props.file.id)
setIsDragged(true)
}
// Handle drag enter
const handleDragEnter = (e) => {
// Only set as target if not equal to source
if (!isDragged) {
setDragCounter(dragCounter => dragCounter + 1)
}
}
//Handle drag end
const handleDragEnd = (e) => {
e.preventDefault()
setIsDragged(false)
}
// Handle drag exit
const handleDragLeave = () => {
// Only remove as target if not equal to source
if (!isDragged) {
setDragCounter(dragCounter => dragCounter - 1)
}
}
// Handle drag over
const handleDragOver = (e) => {
e.preventDefault()
}
// Handle drag drop
const onDragDropFunctions = (e) => {
setDragCounter(0)
// Only trigger when target if not equal to source
if (!isDragged) {
props.onDrop({
id: props.file.id,
display_type: 'file'
})
}
}

return (
<div
className={`${styles.card} ${dragCounter !== 0 && styles.is_hovered} ${isDragged && styles.is_dragged}`}
test={console.log('render')}
draggable={draggable}
onDragStart={onDragStartFunctions}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragEnd={handleDragEnd}
onDragLeave={handleDragLeave}
onDrop={onDragDropFunctions}
>
<div className={styles.cardInner}>
<div className={styles.videoContainer} onClick={() => props.handleActiveMedia(props.file, 'show')}>
{props.file.thumbnail_url && props.file.type === 'video' &&
<MdPlayCircleFilled className={styles.playButton} />
}
{!props.file.thumbnail_url && props.file.type === 'image' &&
<MdImage className={styles.processingButton} />
}
{!props.file.thumbnail_url && props.file.type === 'video' &&
<FaVideo className={styles.processingButton} />
}
<div className={styles.image} style={props.file.thumbnail_url && { backgroundImage: `url(${props.file.thumbnail_url})` }}></div>
</div>
<div className={styles.body}>
<div className={styles.main}>
{!inputActive ?
<p className={styles.title}>{title}</p>
:
<input
ref={input}
className={styles.titleInput}
type="text"
onKeyPress={handleKeyPress}
onChange={handleChange}
onBlur={handleFocusLoss}
defaultValue={title}
/>
}
</div>
<ToggleContext onClick={() => setMenuActive(prevMenuActive => !prevMenuActive)}>
{
menuActive && <div className={styles.menuBackground} />
}
<Dropdown top small active={menuActive}>
<ButtonLight title={'Rename'} icon={<MdTitle />} onClick={handleRename} />
<ButtonLight title={'Label'} icon={<MdLabel />} onClick={() => props.handleActiveMedia(props.file, 'label')} />
<ButtonLight title={'Share'} icon={<MdShare />} onClick={() => window.alert("Sharing is not yet supported. Stay put.")} />
{/*props.file.type === 'video' && <ButtonLight title={'Split'} icon={<RiScissorsFill />} />*/}
<ButtonLightConfirm
danger
title={'Delete'}
icon={<MdDelete />}
onClick={(e) => preventDefault(e)}
confirmAction={handleDelete}
preventDrag={() => setDraggable(false)}
enableDrag={() => setDraggable(true)}
/>
</Dropdown>
</ToggleContext>
</div>
</div>
</div>
);
}

这是设置动画的css:

.is_hovered {
box-shadow: 0 0 0 3px var(--blue);
}
.is_hovered > div {
transform: scale(0.9);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.08);
transition: .1s;
}

编辑:添加代码

第2版:更新示例视频以显示重新渲染

我认为您应该首先尝试使用useCallback"记忆"所有函数。特别是当您将其中一些函数传递给其他组件时,它们可能会导致DOM中不必要的更深层次的重新渲染。

我不知道你是否熟悉useCallback,但基本上它只是围绕着你的函数,只有当特定值发生变化时才会更新它。这允许React避免在每次渲染时重新创建它,并避免导致DOM中更深层次的组件重新渲染。

您可以在这里阅读文档,但要点是,您将编写getA = useCallback(() => a, [a]),而不是const getA = () => a,并且数组包含函数的所有依赖项,这些依赖项会导致函数在更改时更新。

请确保在JSX中使用这些函数,并避免使用onClick={(e) => preventDefault(e)}之类的箭头函数。您所调用的preventDefault函数甚至可以完全位于组件之外,因为它不引用任何特定于组件的内容。

尝试进行这些更新,看看它是否会有所不同。也可以在没有console.log的情况下进行测试,因为这也会减慢速度。

最新更新