[译] 优化 React APP 的 10 种方法

时间:2022-07-22
本文章向大家介绍[译] 优化 React APP 的 10 种方法,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

如何优化性能以提供出色的用户体验。

在开发任何软件(尤其是Web应用程序)时,优化是每个开发人员考虑的第一件事。像Angular,React等其他JS框架都包含了一些很棒的配置和功能。在这里,我将回顾有助于您优化应用性能的功能和技巧。

无论您使用哪种特定的模式和方法来优化代码。保持 DRY 原则是非常重要的。始终努力重用组件-保证可以帮助编写优化的代码。如果您花费更多的时间来编写出色的代码,而花费更少的时间来编写平庸的代码(出错的机会更大),那么奇妙的事情将会发生。

话虽如此,在处理大型代码库或使用不同的存储库时,重用代码可能会成为真正的挑战,这主要有两个原因:1.您通常不知道有用的代码段。2.跨存储库共享代码的传统方式是通过软件包,这需要一些繁重的配置。要解决这两个问题,请使用 BitGitHub )之类的工具。Bit可帮助您将组件与代码库隔离,并在 bit.dev 上共享它们。令人印象深刻的搜索引擎,过滤器和实时游乐场可轻松找到 bit.dev 上的组件。—好的代码始于良好的工作习惯。

示例:搜索在bit.dev上共享的React组件

1. useMemo()

这是一个React钩子,用于在React中消耗大量CPU资源的函数中进行缓存。

让我们来看一个例子:

function App() {
    const [count, setCount] = useState(0)
    
    const expFunc = (count)=> {
        waitSync(3000);
        return count * 90;
    }
    const resCount = expFunc(count)
    return (
        <>
            Count: {resCount}
            <input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
        </>
    )
}

我们有一个昂贵的函数expFunc,需要3分钟才能执行,它需要输入count等待3分钟才能返回的倍数90。我们有一个变量resCount,expFunc该count变量从useState挂钩中调用。我们有一个输入,可以count在键入任何内容时设置状态。

每当我们键入任何内容时,我们的应用程序组件都会重新渲染,从而导致该expFunc函数被调用。我们将看到,如果连续输入,该函数将被调用,从而导致巨大的性能瓶颈。对于每个输入,渲染将花费3分钟。如果键入3,则expFunc将运行3分钟,如果3再次键入,将再次花费3分钟。它不应在第二个输入中再次运行,因为它与前一个输入相同,它应将结果存储在某个位置,然后在不运行函数(expFunc)的情况下将其返回。

在这里,我们将使用useMemo挂钩为我们优化expFunc。useMemo具有以下结构:

useMemo(()=> func, [input_dependency])

Func是我们要缓存/记忆的函数,input_dependency是useMemo将针对其缓存的func的输入数组,也就是说,如果它们更改了func,则将被调用。

现在,在我们的功能组件App上使用useMemo:

function App() {
    const [count, setCount] = useState(0)
    
    const expFunc = (count)=> {
        waitSync(3000);
        return count * 90;
    }
    const resCount = useMemo(()=> {
        return expFunc(count)
    }, [count])
    return (
        <>
            Count: {resCount}
            <input type="text" onChange={(e)=> setCount(e.target.value)} placeholder="Set Count" />
        </>
    )
}

现在,当再次发生相同的输入时,这里将针对输入缓存expFunc结果useMemo将跳过调用expFunc并返回针对输入缓存的输出。

这将使App组件高度优化。

请参阅,该useMemo缓存技术可提高性能。同样,它可以用于根据其属性缓存功能组件。

2.虚拟化长列表

如果呈现大型数据列表,建议一次在浏览器的可见视口内仅呈现一小部分数据集,然后在列表滚动时呈现下一个数据,这称为“窗口” 。为此已经构建了很棒的React库, 反应窗口反应虚拟化 由Brian Vaughn撰写。

3. React.PureComponent

就像shouldComponentUpdate对组件进行分类一样,React.PureComponent也是如此。

React.PureComponent是基础组件类,用于检查状态字段和属性以了解是否应更新组件。

让我们在shouldComponentUpdate部分中转换我们的示例

class ReactComponent extends Component {
    constructor(props, context) {
        super(props, context)
        this.state = {
            data: null
        }
        this.inputValue = null
    }
    handleClick = () => {
        this.setState({data: this.inputValue})
    }
    onChange = (evt) => {
        this.inputValue = evt.target.value
    }
    shouldComponentUpdate( nextProps,nextState) {
        if(nextState.data === this.state.data)
            return false
        return true
    }
    render() {
        l("rendering App")
        return (
            <div>
                {this.state.data}
                <input onChange={this.onChange} />
                <button onClick={this.handleClick}>Click Me </button>
            </div>
        )
    }
}

使用React.PureComponent:

class ReactComponent extends React.PureComponent {
    constructor(props, context) {
        super(props, context)
        this.state = {
            data: null
        }
        this.inputValue = null
    }
    handleClick = () => {
        this.setState({data: this.inputValue})
    }
    onChange = (evt) => {
        this.inputValue = evt.target.value
    }
    render() {
        l("rendering App")
        return (
            <div>
                {this.state.data}
                <input onChange={this.onChange} />
                <button onClick={this.handleClick}>Click Me </button>
            </div>
        )
    }
}

看到,我们删除了shouldComponentUpdate并使ReactComponent扩展了React.PureComponent。

在文本框中输入2并Click Me连续单击按钮,我们将看到ReactComponent将被重新渲染一次,并且永远不会被渲染。

它将上一个道具和状态对象的字段与下一个道具和状态对象的字段进行浅层比较。它不只是对它们进行对象引用比较。

React.PureComponent通过减少浪费的渲染次数来优化我们的组件。

4.缓存功能

可以在render方法的React组件JSX中调用函数。

function expensiveFunc(input) {
    ...
    return output
}
class ReactCompo extends Component {
    render() {
        return (
            <div>
                {expensiveFunc}
            </div>
        )
    }
}

如果这些功能变得昂贵,即执行时间很长,它将挂起其余的重新渲染代码以完成操作,从而妨碍了用户的体验。

参见,在ReactCompo中。cheapableFunc在JSX中呈现,对于每次重新呈现,都会调用该函数,并将返回值呈现在DOM上。该函数占用大量CPU,我们将看到在每次重新渲染时都会调用该函数,React将不得不等待其完成才能运行其余的重新渲染算法。

最好的办法是针对输出缓存功能的输入,以便当再次发生相同的输入时,函数的连续执行变得更快。

function expensiveFunc(input) {
    ...
    return output
}
const memoizedExpensiveFunc = memoize(expensiveFunc)
class ReactCompo extends Component {
    render() {
        return (
            <div>
                {memoizedExpensiveFunc}
            </div>
        )
    }
}

5.使用重新选择选择器

使用 reselect 仓库优化我们的Redux状态管理。由于Redux实行不变性,这意味着每次操作分派时都会创建新的对象引用。这将影响性能,因为即使对象引用发生更改但字段未更改,也会在组件上触发重新渲染。

重新选择库封装了Redux状态并检查该状态的字段,并告诉React什么时候渲染或不渲染字段。

因此,重新选择可通过浅遍遍遍prev和当前Redux状态字段来检查宝贵的时间,尽管它们具有不同的内存引用,但它们是否已更改。如果字段已更改,它将告诉React重新渲染;如果没有字段已更改,则尽管创建了新的状态对象,它也会取消重新渲染。

6. 使用 Web worker

JS代码在单个线程上运行。在同一线程上运行一个长进程将严重影响UI呈现代码,因此最好的选择是将进程移至另一个线程。这是由Web工作人员完成的。它们是我们可以在其中创建线程并与主线程并行运行而不妨碍UI流程的网关。

我们可以在React中使用Web worker,尽管没有官方支持,但是有一些方法可以将Web worker添加到React应用中。让我们来看一个例子:

// webWorker.js
const worker = (self) => {
    function generateBigArray() {
        let arr = []
        arr.length = 1000000
        for (let i = 0; i < arr.length; i++)
            arr[i] = i
        return arr
    }
    function sum(arr) {
        return arr.reduce((e, prev) => e + prev, 0)
    }
    function factorial(num) {
        if (num == 1)
            return 1
        return num * factorial(num - 1)
    }
    self.addEventListener("message", (evt) => {
        const num = evt.data
        const arr = generateBigArray()
        postMessage(sum(arr))
    })
}
export default worker

// App.js
import worker from "./webWorker"
import React, { Component } from 'react';
import './index.css';
class App extends Component {
    constructor() {
        super()
        this.state = {
            result: null
        }
    }
    calc = () => {
        this.webWorker.postMessage(null)
    }
    componentDidMount() {
        let code = worker.toString()
        code = code.substring(code.indexOf("{") + 1, code.lastIndexOf("}"))
        const bb = new Blob([code], { type: "application/javascript" });
        this.webWorker = new Worker(URL.createObjectURL(bb))
        this.webWorker.addEventListener("message", (evt) => {
            const data = evt.data
            this.setState({ result: data })
        })
    }
    render() {
        return ( 
            <div>
                <button onClick = { this.calc }> Sum </button>   
                <h3> Result: { this.state.result }</h3>  
            </div>
        )
    }
}

此应用程序将计算包含1M个元素的数组的总和,现在,如果我们在主线程中执行了此操作,则主线程将一直挂起,直到遍历1M个元素并计算了它们的总和。

现在,在这里我们将其移至Web worker,我们的主线程将与web worker线程并行运行,同时将计算1M元素数组的总和。完成后将传达结果,并且主线程将仅呈现结果。快速,简单和高性能。

7.延迟加载

延迟加载已成为现在广泛用于加快加载时间的优化技术之一。延迟加载的前景有助于将某些Web应用程序性能问题的风险降至最低。

为了在React中延迟加载路由组件,使用了React.lazy()API。

延迟加载已成为现在广泛用于加快加载时间的优化技术之一。延迟加载的前景有助于将某些Web应用程序性能问题的风险降至最低。

为了在React中延迟加载路由组件,使用了React.lazy()API。

这里引用我之前博客的内容:

React.lazy是Reactv16.6发布时添加到React的新功能,它为延迟加载和代码拆分React组件提供了一种简单明了的方法。

React.lazy函数使您可以将动态导入呈现为常规组件。— React博客

React.lazy使创建组件和使用动态导入呈现组件变得容易。React.lazy将一个函数作为参数:

React.lazy(()=>{})
// or
function cb () {}
React.lazy(cb)

此回调函数必须使用动态import()语法加载组件的文件:

// MyComponent.js
class MyComponent extends Component{
    render() {
        return <div>MyComponent</div>
    }
}
const MyComponent = React.lazy(()=>{import('./MyComponent.js')})
function AppComponent() {
    return <div><MyComponent /></div>
}
// or
function cb () {
    return import('./MyComponent.js')
}
const MyComponent = React.lazy(cb)
function AppComponent() {
    return <div><MyComponent /></div>
}

React.lazy的回调函数通过import()调用返回一个Promise 。Promise会解决模块是否成功加载的问题,并拒绝由于网络故障,错误的路径解析,找不到文件等原因导致模块加载错误。

当webpack遍历我们的代码进行编译和捆绑时,当它到达React.lazy()和时会创建一个单独的捆绑import()。我们的应用程序将变成这样:

react-app
 dist/
  - index.html
  - main.b1234.js (contains Appcomponent and bootstrap code)
  - mycomponent.bc4567.js (contains MyComponent)
/** index.html **/
<head>
    <div id="root"></div>
    <script src="main.b1234.js"></script>
</head>

现在,我们的应用程序现在分为多个捆绑包。呈现AppComponent时,将加载mycomponent.bc4567.js文件,并且包含的 MyComponent将显示在DOM上。

8. React.memo()

就像useMemo和React.PureComponent一样,React.memo() 用于记忆/缓存功能组件。

function My(props) {
    return (
        <div>
            {props.data}
        </div>
    )
}
function App() {
    const [state, setState] = useState(0)
    return (
        <>
            <button onClick={()=> setState(0)}>Click</button>
            <My data={state} />
        </>
    )
}

应用程序渲染My组件,通过dataprop 将状态传递给My 。现在,看到按下按钮时,该按钮会将状态设置为0。如果连续按下按钮,则状态始终保持不变,但是尽管传递给其道具的状态相同,但My组件仍将重新渲染。如果App和My下有成千上万个组件,这将是一个巨大的性能瓶颈。

为了减少这种情况,我们将用React.memo包装My组件,该组件将返回My的备注版本,该版本将在App中使用。

function My(props) {
    return (
        <div>
            {props.data}
        </div>
    )
}
const MemoedMy = React.memo(My)
function App() {
    const [state, setState] = useState(0)
    return (
        <>
            <button onClick={()=> setState(0)}>Click</button>
            <MemeodMy data={state} />
        </>
    )
}

这样,连续按下“单击”按钮将仅触发一次“永不”的重新渲染。这是因为React.memo会记住其道具,并会在不执行My组件的情况下返回缓存的输出,只要相同的输入一遍又一遍。

React.PureComponent对组件进行分类是Reat.memo对功能组件进行分类。

9. useCallback()

在上一篇文章中: 使用useMemo,提高功能组件的性能useCallback

它可以用作useMemo,但区别在于它用于记忆函数声明。

假设我们有这个:

function TestComp(props) {
    l('rendering TestComp')
    return (
        <>
            TestComp
            <button onClick={props.func}>Set Count in 'TestComp'</button>
        </>
    )
}
TestComp = React.memo(TestComp)
function App() {
    const [count, setCount] = useState(0)
    return (
        <>
            <button onClick={()=> setCount(count + 1)}>Set Count</button>
            <TestComp func={()=> setCount(count + 1)} />
        </>
    )
}

我们有一个App组件,它使用useState维护计数状态,每当调用setCount函数时,App组件都会重新呈现。它呈现一个按钮和TestComp组件,如果我们单击Set Count按钮,则App组件将连同其子树一起重新呈现。现在,使用备忘录对TestComp进行备忘录化,以避免不必要的重新渲染。React.memo通过将其当前/下一个道具与上一个道具进行比较来记住一个组件,如果它们相同,则不会重新渲染该组件。TestComp会在func props属性中实际上接收到一个props函数,每当重新渲染App时,都会检查TestComp的props函数是否相同,如果发现相同,则不会重新渲染。

这里的问题是TestComp接收到函数prop的新实例。怎么样?看一下JSX:

...
    return (
        <>
            ...
            <TestComp func={()=> setCount(count + 1)} />
        </>
    )
...

传递了箭头函数声明,因此,每当呈现App时,总是使用新的引用(内存地址指针)创建新的函数声明。因此,React.memo的浅表比较将记录差异,并为重新渲染提供批准。

现在,我们如何解决这个问题?如果我们将函数移到函数范围之外,那会很好,但是不会引用setCount函数。这是useCallback出现的地方,我们将把功能道具传递给useCallback并指定依赖项,useCallback钩子返回函数式道具的记忆版本,这就是我们将传递给TestComp的东西。

function App() {
    const check = 90
    const [count, setCount] = useState(0)
    const clickHndlr = useCallback(()=> { setCount(check) }, [check]);
    return (
        <>
            <button onClick={()=> setCount(count + 1)}>Set Count</button>
            <TestComp func={clickHndlr} />
        </>
    )
}

在这里,除非clickHndlr重新定义App依赖关系check,否则不会在每次重新渲染组件时都重新创建它,因此当我们反复单击Set Count按钮TestComp时不会重新渲染。useCallback将检查check变量,如果不相同,其上一个值,它将返回函数传递所以TestComp和React.memo会看到一个新的参考和重新渲染TestComp,如果不一样useCallback就什么都不返回所以React.memo会看到一个函数引用相同的分组值并取消重新呈现TestComp。

10. shouldComponentUpdate()

React应用程序由组件组成,从根组件(通常是App.js中的App)到扩展分支。

class ReactComponent extends Component {
    render() {
        return (
            <div></div>
        )
    }
}

基本的React组件。

这些组件树使其具有父子关系,即在组件中更新绑定数据时,将重新呈现该组件及其子组件,以使更改传播到整个子组件树中。当要重新渲染组件时,React会将其先前的数据(属性和上下文)与当前数据(属性和上下文)进行比较,如果它们相同,则不会进行重新渲染,但是如果存在差异,则该组件并重新渲染其子级。

由于props和context是对象,因此React使用严格相等运算符===通过对象引用比较差异。因此,React使用该引用来知道先前的道具和状态何时与当前的道具和状态发生了变化。

class ReactComponent extends Component {
    constructor(props, context) {
        super(props, context)
        this.state = {
            data: null
        }
        this.inputValue = null
    }
handleClick = () => {
        this.setState({data: this.inputValue})
    }
onChange = (evt) => {
        this.inputValue = evt.target.value
    }
render() {
        l("rendering App")
        return (
            <div>
                {this.state.data}
                <input onChange={onChange} />
                <button onClick={handleCick}>Click Me </button>
            </div>
        )
    }
}

请参阅上面的组件。它在状态对象中具有数据。如果我们在输入文本框中输入一个值并按下Click Me按钮,则将呈现输入中的值。我特意在render方法中添加了l(“ rendering App”),以便我们知道ReactComponent何时渲染。

现在,如果我们输入2并单击按钮,则将渲染组件,应该渲染该组件,因为先前的状态是这样的:

state = { data: null }

下一个状态对象是这样的:

state = { data: 2 }

因为setState每次调用都会创建新的状态对象,所以严格相等运算符将看到不同的内存引用并触发组件上的重新呈现。

如果再次单击该按钮,我们将有另一个重新渲染,不是这样,因为前一个状态对象和下一个状态对象将具有相同的data值,但是由于setState新状态对象的创建,React将看到差异状态对象引用和触发器重新呈现,尽管它们具有相同的内部值。

现在,如果组件树增长到数千个组件,则此重新渲染可能会很昂贵。

这样,React为我们提供了一种方法来控制组件的重新渲染,而不是通过React来控制内部逻辑,这是shouldComponentUpdate方法。只要重新渲染组件,就会调用shouldComponentUpdate,如果返回true,则重新渲染组件;如果为false,则取消重新渲染。

我们将shouldComponentUpdate添加到ReactComponent。此方法接受下一个状态对象和下一个props对象作为参数,因此使用此方法,我们将实现检查以告知React什么时候重新渲染。

class ReactComponent extends Component {
    constructor(props, context) {
        super(props, context)
        this.state = {
            data: null
        }
        this.inputValue = null
    }
handleClick = () => {
        this.setState({data: this.inputValue})
    }
onChange = (evt) => {
        this.inputValue = evt.target.value
    }
shouldComponentUpdate( nextProps,nextState) {
        if(nextState.data === this.state.data)
            return false
        return true
    }
render() {
        l("rendering App")
        return (
            <div>
                {this.state.data}
                <input onChange={this.onChange} />
                <button onClick={this.handleClick}>Click Me </button>
            </div>
        )
    }
}

看到,在shouldCmponentUpdate中,我检查了下一个状态对象nextState对象和当前状态对象中的数据值。如果不相等,则返回true,将触发重新渲染;如果不相等,则返回false,以取消重新渲染。

再次运行该应用程序,输入2并连续单击该Click Me按钮,您将看到渲染一次,不再进行:)

看到,我们使用了shouldComponentUpdate方法来设置何时重新渲染组件,从而有效地提高了组件的性能。

结论

React很棒!

我们在这里提到的技巧绝不能全部实现。请记住,不要及早进行优化,首先对项目进行编码,然后在必要时进行优化。

谢谢!!!