【问题标题】:React performance problems with increasing number of components随着组件数量的增加反应性能问题
【发布时间】:2021-04-03 21:50:16
【问题描述】:

原来问题出在我的电脑上。不过,James 对于如何隔离问题并利用 useCallback 和 useMemo 进行优化提出了一些很好的观点。

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

This is the demo video

这里有一些提示

  • 我没有不必要的重新渲染。只有单个组件在悬停时才会被渲染。
  • 动画仅限于悬停元素的容器 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;
}

编辑:添加代码

Edit2:更新示例视频以显示重新渲染

【问题讨论】:

    标签: javascript reactjs performance animation


    【解决方案1】:

    我认为您应该首先尝试的是使用useCallback 来“记忆”您的所有功能。尤其是当您将其中一些函数传递给其他组件时,它们可能会导致在 DOM 中进行不必要的重新渲染。

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

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

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

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

    【讨论】:

    • 其实我不知道useCallback。我实施了您的建议并包装了所有功能。它确实使性能略有提高,但不幸的是没有什么大不了的。让我感到困惑的是,当开发工具告诉我只有被悬停的卡片被重新渲染时,性能在某种程度上与页面上卡片元素的数量有关。
    • 好的,所以我注释掉了包装 div 中的所有内容(但留下了 dragEvents)。一切都像这样顺利进行,即使屏幕上有很多卡片。你还想要那个上下文中的父代码吗?此外,卡片组件内部是否有任何让您印象深刻的昂贵操作?诸如将组件作为道具传递等事情。我是 React 新手,所以我可能不知道一堆对我来说似乎没问题但可能对性能有害的事情。我还在原始问题中编辑了视频,以便您可以看到渲染闪烁。非常感谢您对此进行调查!
    • 我刚刚完成了对子元素的逐一和分组停用。似乎每个孩子都对整体放缓做出了一定程度的贡献。让我感到困惑的是,当开发工具显示除了与之交互的卡片之外,没有任何卡片会重新渲染时,屏幕上卡片组件的数量会对性能产生相关的负面影响。特别是考虑到父组件不会改变,因此不会触发子组件的任何更改。
    • 这里没有什么大秘密。随意查看存储库github.com/maxibenner/cardboard。罪魁祸首是“browse”容器和“CardFile”组件。您可以使用自述文件中的测试凭据直接进入包含大量文件的帐户。
    • 感谢您的提醒,我删除了实时密钥。考虑到开发密钥的风险是值得的,因此人们可以立即运行它。您是否在更快的计算机上运行它?我的是 5 年的入门级 MacBook Pro。不是最快的,但我假设它应该能够处理具有 60fps 基本 css 动画的小型 React 应用程序。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-06-08
    • 2021-02-08
    • 2018-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-12
    相关资源
    最近更新 更多