• [React] 性能优化相关 (一)


    1.React.memo

    父组件被重新渲染的时候,也会触发子组件的重新渲染,这样就多出了无意义的性能开销。如果子组件的状态没有发生变化,则子组件是不需要被重新渲染的。

    const 组件 = React.memo(函数式组件)
    
    • 1

    父组件声明了 count 和 flag 两个状态,子组件依赖于父组件通过 props 传递的 num。当父组件修改 flag 的值时,会导致子组件的重新渲染。

    import React, { useEffect, useState } from 'react'
    
    // 父组件
    export const Father: React.FC = () => {
      // 定义 count 和 flag 两个状态
      const [count, setCount] = useState(0)
      const [flag, setFlag] = useState(false)
    
      return (
        <>
          <h1>父组件</h1>
          <p>count 的值是:{count}</p>
          <p>flag 的值是:{String(flag)}</p>
          <button onClick={() => setCount((prev) => prev + 1)}>+1</button>
          <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>
          <hr />
          <Son num={count} />
        </>
      )
    }
    
    // 子组件:依赖于父组件通过 props 传递进来的 num
    export const Son: React.FC<{ num: number }> = ({ num }) => {
      useEffect(() => {
        console.log('触发了子组件的渲染')
      })
      return (
        <>
          <h3>子组件 {num}</h3>
        </>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    React.memo(函数式组件) 将子组件包裹起来, 只有子组件依赖的 props 发生变化的时候, 才会触发子组件的重新渲染。

    // 子组件:依赖于父组件通过 props 传递进来的 num
    export const Son: React.FC<{ num: number }> = React.memo(({ num }) => {
      useEffect(() => {
        console.log('触发了子组件的渲染')
      })
      return (
        <>
          <h3>子组件 --- {num}</h3>
        </>
      )
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    2.useMemo

    在 Father 组件中添加一个“计算属性”, 根据 flag 值的真假, 返回不同的内容。

    // 父组件
    export const Father: React.FC = () => {
      // 定义 count 和 flag 两个状态
      const [count, setCount] = useState(0)
      const [flag, setFlag] = useState(false)
    
      // 根据布尔值进行计算,动态返回内容
      const tips = () => {
        console.log('触发了 tips 的重新计算')
        return flag ? <p>1</p> : <p>2</p>
      }
    
      return (
        <>
          <h1>父组件</h1>
          <p>count 的值是:{count}</p>
          <p>flag 的值是:{String(flag)}</p>
          {tips()}
          <button onClick={() => setCount((prev) => prev + 1)}>+1</button>
          <button onClick={() => setFlag((prev) => !prev)}>Toggle</button>
          <hr />
          <Son num={count} />
        </>
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    点击父组件中的 +1 按钮, 发现 count 在自增, flag 不会改变, 此时也会触发 tips 函数的重新执行, 造成性能的浪费。
    希望如果 flag 没有发生变化, 则避免 tips 函数的重新计算, 从而优化性能。此时需要用到 React Hooks 提供的 useMemo API。

    useMemo()

    const memoValue = useMemo(() => {
      return 计算得到的值
    }, [value]) // 表示监听 value 的变化
    
    • 1
    • 2
    • 3
    • 第一个参数为函数, 用于处理计算的逻辑, 必须用到得到的值。
    • 第二个参数为数组, 为依赖项, 依赖项发生变化, 触发之前的第一个参数的函数的执行。如果不传的话, 每次更新都会重新计算。如果传入的是[], 那么只会第一个render的时候触发, 以后不会重新渲染。
    import React, { useEffect, useState, useMemo } from 'react'
    
    // 根据布尔值进行计算,动态返回内容
    const tips = useMemo(() => {
     console.log('触发了 tips 的重新计算')
     return flag ? 1 : 2
    }, [flag])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    此时点击+1, 不会触发tips的重新计算。

    3.useCallback

    useMemo 能够达到缓存某个变量值的效果, 而当前要学习的 useCallback 用来对组件内的函数进行缓存, 返回的是函数。

    useCallback 会返回一个 memorized 回调函数供组件使用, 从而防止组件每次 rerender 时反复创建相同的函数, 节省内存, 提高性能。

    • 第一个参数为一个函数, 处理业务逻辑, 这个函数就是需要被缓存的函数

    • 第二个参数为依赖项列表, 依赖项变化时才会重新执行 useCallback。如果省略的话, 每次更新都会重新计算, 如果为[], 就只是第一次render的时候触发一次。

    当输入框触发 onChange 事件时, 会给 kw 重新赋值。kw 值的改变会导致组件的 rerender, 组件的 rerender 会导致反复创建 onKwChange 函数并添加到 Set 集合, 造成了不必要的内存浪费。

    import React, {useState} from 'react';
    
    // 用来存储函数的 set 集合
    const set = new Set()
    
    const Search:React.FC = () => {
        const [kw, setKw] = useState('')
    
        const onKwChange = (e: React.ChangeEvent<HTMLInputElement>) => {
            setKw(e.currentTarget.value)
        }
    
        // 把 onKwChange 函数的引用,存储到 set 集合中
        set.add(onKwChange)
        // 打印 set 集合中元素的数量
        console.log('set 中函数的数量为:' + set.size)
    
        return (
            <>
                <input type="text" value={kw} onChange={onKwChange} />
                <hr />
                <p>{kw}</p>
                <p></p>
            </>
        )
    }
    
    export default Search;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    每次文本框的值发生变化, 会打印set.size的值, 这个值一直在自增 +1, 因为每次组件 rerender 都会创建一个新的 onKwChange 函数添加到 set 集合中。

    为了防止 Search 组件 rerender 时每次都会重新创建 onKwChange 函数, 可以使用 useCallback 对这个函数进行缓存。

    import React, {useCallback, useState} from 'react';
    
    // 用来存储函数的 set 集合
    const set = new Set()
    
    const Search:React.FC = () => {
        const [kw, setKw] = useState('')
    
        const onKwChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
            setKw(e.currentTarget.value)
        }, [])
    
        // 把 onKwChange 函数的引用,存储到 set 集合中
        set.add(onKwChange)
        // 打印 set 集合中元素的数量
        console.log('set 中函数的数量为:' + set.size)
    
        return (
            <>
                <input type="text" value={kw} onChange={onKwChange} />
                <hr />
                <p>{kw}</p>
                <p></p>
            </>
        )
    }
    
    export default Search;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    无论 input 的值如何发生变化, 每次打印的 set.size 的值都是 1。证明我们使用 useCallback 实现了对函数的缓存。

    4.useTransition

    useTransition 可以将一个更新转为低优先级更新, 使其可以被打断, 不阻塞 UI 对用户操作的响应, 能够提高用户的使用体验。它常用于优化视图切换时的用户体验。

    Home、Movie、About 3个标签, 其中Movie 是一个很耗性能的组件, 在渲染 Movie 组件期间页面的 UI 会被阻塞, 用户会感觉页面十分卡顿。

    import React, { useState } from 'react'
    
    export const TabsContainer: React.FC = () => {
      // 被激活的标签页的名字
      const [activeTab, setActiveTab] = useState('home')
    
      // 点击按钮,切换激活的标签页
      const onClickHandler = (tabName: string) => {
        setActiveTab(tabName)
      }
    
      return (
        <div style={{ height: 500 }}>
          <TabButton isActive={activeTab === 'home'} onClick={() => onClickHandler('home')}>
            首页
          </TabButton>
          <TabButton isActive={activeTab === 'movie'} onClick={() => onClickHandler('movie')}>
            电影
          </TabButton>
          <TabButton isActive={activeTab === 'about'} onClick={() => onClickHandler('about')}>
            关于
          </TabButton>
          <hr />
    
          {/* 根据被激活的标签名,渲染对应的 tab 组件 */}
          {activeTab === 'home' && <HomeTab />}
          {activeTab === 'movie' && <MovieTab />}
          {activeTab === 'about' && <AboutTab />}
        </div>
      )
    }
    
    // Button 组件 props 的 TS 类型
    type TabButtonType = React.PropsWithChildren & { isActive: boolean; onClick: () => void }
    // Button 组件
    const TabButton: React.FC<TabButtonType> = (props) => {
      const onButtonClick = () => {
        props.onClick()
      }
    
      return (
        <button className={['btn', props.isActive && 'active'].join(' ')} onClick={onButtonClick}>
          {props.children}
        </button>
      )
    }
    
    // Home 组件
    const HomeTab: React.FC = () => {
      return <>HomeTab</>
    }
    
    // Movie 组件
    const MovieTab: React.FC = () => {
      const items = Array(100000)
        .fill('MovieTab')
        .map((item, i) => <p key={i}>{item}</p>)
      return items
    }
    
    // About 组件
    const AboutTab: React.FC = () => {
      return <>AboutTab</>
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    .btn {
      margin: 5px;
      background-color: rgb(8, 92, 238);
      color: #fff;
      transition: opacity 0.5s ease;
    }
    
    .btn:hover {
      opacity: 0.6;
      transition: opacity 0.5s ease;
    }
    
    .btn.active {
      background-color: rgb(3, 150, 0);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    语法结构

    import { useTransition } from 'react';
    
    function TabContainer() {
      const [isPending, startTransition] = useTransition();
      // ……
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • isPending 布尔值: 是否存在待处理的 transition, 如果值为 true, 说明页面上存在待渲染的部分, 可以给用户一个加载的提示。
    • startTransition 函数: 调用此函数, 可以将状态的更新标记为低优先级的, 不阻塞UI 对用户操作的响应。

    使用 useTransition 把点击按钮后为 activeTab 赋值的操作, 标记为低优先级, UI 不被阻塞。

    import React, { useState, useTransition } from 'react'
    
    export const TabsContainer: React.FC = () => {
      // 被激活的标签页的名字
      const [activeTab, setActiveTab] = useState('home')
      const [, startTransition] = useTransition()
    
      // 点击按钮,切换激活的标签页
      const onClickHandler = (tabName: string) => {
        startTransition(() => {
          setActiveTab(tabName)
        })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    点击 Movie 按钮后, 状态的更新被标记为低优先级, About 按钮的 hover 效果和点击操作都会被立即响应。

    5.useDeferredValue

    useDeferredValue 提供一个 state 的延迟版本, 根据其返回的延迟的 state 能够推迟更新 UI 中的某一部分。

    import { useState, useDeferredValue } from 'react';
    
    function SearchPage() {
      const [kw, setKw] = useState('');
      // 根据 kw 得到延迟的 kw
      const deferredKw = useDeferredValue(kw);
      // ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    SearchResult 组件会根据用户输入的关键字, 循环生成大量的p标签, 会浪费性能。

    import React, { useState } from 'react'
    
    // 父组件
    export const SearchBox: React.FC = () => {
      const [kw, setKw] = useState('')
    
      const onInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setKw(e.currentTarget.value)
      }
    
      return (
        <div style={{ height: 500 }}>
          <input type="text" value={kw} onChange={onInputChange} />
          <hr />
          <SearchResult query={kw} />
        </div>
      )
    }
    
    // 子组件,渲染列表项
    const SearchResult: React.FC<{ query: string }> = (props) => {
      if (!props.query) return
      const items = Array(40000)
        .fill(props.query)
        .map((item, i) => <p key={i}>{item}</p>)
    
      return items
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    优化1
    在这里插入图片描述

    优化2

    当 kw 的值频繁更新时, deferredKw 的值会明显滞后, 此时用户在页面上看到的列表数据并不是最新的, 给内容添加 opacity 透明度, 表明当前看到的内容已过时。

    在这里插入图片描述

  • 相关阅读:
    微信小程序动画
    当下社会和经济形势概述
    【探索Linux】—— 强大的命令行工具 P.14(进程间通信 | 匿名管道 | |进程池 | pipe() 函数 | mkfifo() 函数)
    C语言——自定义类型之枚举
    C++ 核心指南之资源管理(上)概述
    Web前端大作业—个人网页(html+css+javascript)
    Maple希腊字母按键查表
    Openssl数据安全传输平台011:base64的使用
    3.3.2 【MySQL】客户端和服务器通信中的字符集
    智汀智能家居知识普及篇——家居智能控制方式及控制系统的特点,你知道几个?
  • 原文地址:https://blog.csdn.net/qq_43141726/article/details/133420395