使用 react-hooks 在每个渲染器上创建处理程序的性能损失
Performance penalty of creating handlers on every render with react-hooks
我目前对新反应的用例感到非常惊讶 hooks API 以及您可以用它做什么。
实验时出现的一个问题是,总是创建一个新的处理函数只是为了在使用 useCallback
时将其丢弃是多么昂贵。
考虑这个例子:
const MyCounter = ({initial}) => {
const [count, setCount] = useState(initial);
const increase = useCallback(() => setCount(count => count + 1), [setCount]);
const decrease = useCallback(() => setCount(count => count > 0 ? count - 1 : 0), [setCount]);
return (
<div className="counter">
<p>The count is {count}.</p>
<button onClick={decrease} disabled={count === 0}> - </button>
<button onClick={increase}> + </button>
</div>
);
};
虽然我将处理程序包装到 useCallback
中以避免每次渲染时都传递一个新的处理程序,但在大多数情况下,仍然必须创建内联箭头函数才能将其丢弃。
如果我只渲染几个组件,可能没什么大不了的。但是,如果我执行 1000 次,对性能的影响有多大?是否有明显的性能损失?什么是避免它的方法?可能是一个静态处理程序工厂,只有在必须创建新处理程序时才会被调用?
React FAQs给出解释
Are Hooks slow because of creating functions in render?
No. In modern browsers, the raw performance of closures compared to
classes doesn’t differ significantly except in extreme scenarios.
In addition, consider that the design of Hooks is more efficient in a
couple ways:
Hooks avoid a lot of the overhead that classes require, like the cost
of creating class instances and binding event handlers in the
constructor.
Idiomatic code using Hooks doesn’t need the deep component tree
nesting that is prevalent in codebases that use higher-order
components, render props, and context. With smaller component trees,
React has less work to do.
Traditionally, performance concerns around inline functions in React
have been related to how passing new callbacks on each render breaks
shouldComponentUpdate optimizations in child components. Hooks
approach this problem from three sides.
因此,hooks 提供的整体好处远大于创建新函数的代价
此外,对于功能组件,您可以通过使用 useMemo
进行优化,以便组件在其 props 没有变化时重新渲染。
你是对的,在大型应用程序中,这可能会导致性能问题。在将处理程序传递给组件之前绑定处理程序可避免子组件可能进行额外的重新渲染。
<button onClick={(e) => this.handleClick(e)}>click me!</button>
<button onClick={this.handleClick.bind(this)}>click me!</button>
两者是等价的。 e 参数表示 React 事件,而使用箭头函数时,我们必须显式传递它,使用 bind 时任何参数都会自动转发。
但是,如果我执行 1000 次,对性能的影响有多大?是否有明显的性能损失?
这取决于应用程序。如果您只是简单地渲染 1000 行计数器,可能没问题,如下面的代码片段所示。请注意,如果您只是修改个人的状态 <Counter />
,则只会重新呈现该计数器,其他 999 个计数器不受影响。
但我认为你关心的是无关紧要的事情。在现实世界的应用程序中,不太可能呈现 1000 个列表元素。如果您的应用程序必须呈现 1000 个项目,则您设计应用程序的方式可能有问题。
您不应在 DOM 中渲染 1000 个项目。从性能和用户体验的角度来看,无论有没有现代 JavaScript 框架,这通常都是糟糕的。您可以使用窗口技术,只渲染您在屏幕上看到的项目,其他屏幕外的项目可以在内存中。
实施shouldComponentUpdate
(或useMemo
)以便在顶级组件必须重新渲染时不会重新渲染其他项目。
通过使用函数,您可以避免 classes 和其他一些您不知道的 class 相关内容的开销,因为React 会自动为你做这件事。由于在函数中调用了一些挂钩,你损失了一些性能,但你在其他地方也获得了一些性能。
最后,请注意您正在调用 useXXX
挂钩,而不是执行传递给挂钩的回调函数。我确信 React 团队在使 hooks 调用轻量级调用 hooks 不应该太昂贵方面做得很好。
有什么方法可以避免呢?
我怀疑在现实世界中是否会需要创建有状态项一千次。但如果你真的必须这样做,最好将状态提升到父组件中,并将值和 increment/decrement 回调作为道具传递到每个项目中。这样,您的各个项目就不必创建状态修改器回调,而可以简单地使用其父级的回调道具。此外,无状态子组件可以更轻松地实现各种众所周知的性能优化。
最后,我想重申一下,你不应该担心这个问题,因为你应该尽量避免让自己陷入这种情况而不是处理它,利用窗口和分页等技术 - 仅加载您需要在当前页面上显示的数据。
const Counter = ({ initial }) => {
const [count, setCount] = React.useState(initial);
const increase = React.useCallback(() => setCount(count => count + 1), [setCount]);
const decrease = React.useCallback(
() => setCount(count => (count > 0 ? count - 1 : 0)),
[setCount]
);
return (
<div className="counter">
<p>The count is {count}.</p>
<button onClick={decrease} disabled={count === 0}>
-
</button>
<button onClick={increase}>+</button>
</div>
);
};
function App() {
const [count, setCount] = React.useState(1000);
return (
<div>
<h1>Counters: {count}</h1>
<button onClick={() => {
setCount(count + 1);
}}>Add Counter</button>
<hr/>
{(() => {
const items = [];
for (let i = 0; i < count; i++) {
items.push(<Counter key={i} initial={i} />);
}
return items;
})()}
</div>
);
}
ReactDOM.render(
<div>
<App />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
一种方法可能是记住回调以防止子组件的不必要更新。
你可以阅读更多关于这个 here.
此外,我创建了一个 npm 包 useMemoizedCallback,我希望它能帮助任何寻找提高性能解决方案的人。
我用下面的例子做了一个简单的测试,它使用 10k(和 100k)usingCallback
钩子并每 100 毫秒重新渲染一次。好像useCallback
的个数真的很多的时候才会有效果。查看下面的结果。
具有 10k 个钩子的函数组件:
每次渲染耗时8~12ms。
具有 100k 个钩子的函数组件:
每次渲染耗时25~80ms。
Class 具有 10k 个方法的组件:
每次渲染耗时4~5ms。
Class 组件有 100k 个方法:
每次渲染耗时4~6ms。
I've tested with 1k example too. But the profile result looks almost same as the one with 10k.
因此,当我的组件使用 100k 钩子时,我的浏览器中的惩罚很明显,而 class 组件没有显示出明显的差异。所以我想只要您没有使用超过 10k 钩子的组件,它就应该没问题。不过,该数量可能取决于客户端的运行时资源。
测试组件代码:
import React, { useState, useCallback, useEffect } from 'react';
const callbackCount = 10000
const useCrazyCounter = () => {
const callbacks = []
const [count, setCount] = useState(0)
for (let i = 1; i < callbackCount + 1; i++) {
// eslint-disable-next-line
callbacks.push(useCallback(() => {
setCount(prev => prev + i)
// eslint-disable-next-line
}, []))
}
return [count, ...callbacks]
}
const Counter = () => {
const [count, plusOne] = useCrazyCounter()
useEffect(() => {
const timer = setInterval(plusOne, 100)
return () => {
clearInterval(timer)
}}
, [])
return <div><div>{count}</div><div><button onClick={plusOne}>Plus One</button></div></div>
}
class ClassCounter extends React.Component {
constructor() {
super()
this.state = {
count: 0
}
for (let i = 1; i < callbackCount; i++) {
this['plus'+i] = () => {
this.setState(prev => ({
count: prev.count + i
}))
}
}
}
componentDidMount() {
this.timer = setInterval(() => {
this.plus1()
}, 100)
}
componentWillUnmount() {
clearInterval(this.timer)
}
render () {
return <div><div>{this.state.count}</div><div><button onClick={this.plus1}>Plus One</button></div></div>
}
}
const App = () => {
return (
<div className="App">
<Counter/>
{/* <ClassCounter/> */}
</div>
);
}
export default App;
我目前对新反应的用例感到非常惊讶 hooks API 以及您可以用它做什么。
实验时出现的一个问题是,总是创建一个新的处理函数只是为了在使用 useCallback
时将其丢弃是多么昂贵。
考虑这个例子:
const MyCounter = ({initial}) => {
const [count, setCount] = useState(initial);
const increase = useCallback(() => setCount(count => count + 1), [setCount]);
const decrease = useCallback(() => setCount(count => count > 0 ? count - 1 : 0), [setCount]);
return (
<div className="counter">
<p>The count is {count}.</p>
<button onClick={decrease} disabled={count === 0}> - </button>
<button onClick={increase}> + </button>
</div>
);
};
虽然我将处理程序包装到 useCallback
中以避免每次渲染时都传递一个新的处理程序,但在大多数情况下,仍然必须创建内联箭头函数才能将其丢弃。
如果我只渲染几个组件,可能没什么大不了的。但是,如果我执行 1000 次,对性能的影响有多大?是否有明显的性能损失?什么是避免它的方法?可能是一个静态处理程序工厂,只有在必须创建新处理程序时才会被调用?
React FAQs给出解释
Are Hooks slow because of creating functions in render?
No. In modern browsers, the raw performance of closures compared to classes doesn’t differ significantly except in extreme scenarios.
In addition, consider that the design of Hooks is more efficient in a couple ways:
Hooks avoid a lot of the overhead that classes require, like the cost of creating class instances and binding event handlers in the constructor.
Idiomatic code using Hooks doesn’t need the deep component tree nesting that is prevalent in codebases that use higher-order components, render props, and context. With smaller component trees, React has less work to do.
Traditionally, performance concerns around inline functions in React have been related to how passing new callbacks on each render breaks shouldComponentUpdate optimizations in child components. Hooks approach this problem from three sides.
因此,hooks 提供的整体好处远大于创建新函数的代价
此外,对于功能组件,您可以通过使用 useMemo
进行优化,以便组件在其 props 没有变化时重新渲染。
你是对的,在大型应用程序中,这可能会导致性能问题。在将处理程序传递给组件之前绑定处理程序可避免子组件可能进行额外的重新渲染。
<button onClick={(e) => this.handleClick(e)}>click me!</button>
<button onClick={this.handleClick.bind(this)}>click me!</button>
两者是等价的。 e 参数表示 React 事件,而使用箭头函数时,我们必须显式传递它,使用 bind 时任何参数都会自动转发。
但是,如果我执行 1000 次,对性能的影响有多大?是否有明显的性能损失?
这取决于应用程序。如果您只是简单地渲染 1000 行计数器,可能没问题,如下面的代码片段所示。请注意,如果您只是修改个人的状态 <Counter />
,则只会重新呈现该计数器,其他 999 个计数器不受影响。
但我认为你关心的是无关紧要的事情。在现实世界的应用程序中,不太可能呈现 1000 个列表元素。如果您的应用程序必须呈现 1000 个项目,则您设计应用程序的方式可能有问题。
您不应在 DOM 中渲染 1000 个项目。从性能和用户体验的角度来看,无论有没有现代 JavaScript 框架,这通常都是糟糕的。您可以使用窗口技术,只渲染您在屏幕上看到的项目,其他屏幕外的项目可以在内存中。
实施
shouldComponentUpdate
(或useMemo
)以便在顶级组件必须重新渲染时不会重新渲染其他项目。通过使用函数,您可以避免 classes 和其他一些您不知道的 class 相关内容的开销,因为React 会自动为你做这件事。由于在函数中调用了一些挂钩,你损失了一些性能,但你在其他地方也获得了一些性能。
最后,请注意您正在调用
useXXX
挂钩,而不是执行传递给挂钩的回调函数。我确信 React 团队在使 hooks 调用轻量级调用 hooks 不应该太昂贵方面做得很好。
有什么方法可以避免呢?
我怀疑在现实世界中是否会需要创建有状态项一千次。但如果你真的必须这样做,最好将状态提升到父组件中,并将值和 increment/decrement 回调作为道具传递到每个项目中。这样,您的各个项目就不必创建状态修改器回调,而可以简单地使用其父级的回调道具。此外,无状态子组件可以更轻松地实现各种众所周知的性能优化。
最后,我想重申一下,你不应该担心这个问题,因为你应该尽量避免让自己陷入这种情况而不是处理它,利用窗口和分页等技术 - 仅加载您需要在当前页面上显示的数据。
const Counter = ({ initial }) => {
const [count, setCount] = React.useState(initial);
const increase = React.useCallback(() => setCount(count => count + 1), [setCount]);
const decrease = React.useCallback(
() => setCount(count => (count > 0 ? count - 1 : 0)),
[setCount]
);
return (
<div className="counter">
<p>The count is {count}.</p>
<button onClick={decrease} disabled={count === 0}>
-
</button>
<button onClick={increase}>+</button>
</div>
);
};
function App() {
const [count, setCount] = React.useState(1000);
return (
<div>
<h1>Counters: {count}</h1>
<button onClick={() => {
setCount(count + 1);
}}>Add Counter</button>
<hr/>
{(() => {
const items = [];
for (let i = 0; i < count; i++) {
items.push(<Counter key={i} initial={i} />);
}
return items;
})()}
</div>
);
}
ReactDOM.render(
<div>
<App />
</div>,
document.querySelector("#app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>
一种方法可能是记住回调以防止子组件的不必要更新。
你可以阅读更多关于这个 here.
此外,我创建了一个 npm 包 useMemoizedCallback,我希望它能帮助任何寻找提高性能解决方案的人。
我用下面的例子做了一个简单的测试,它使用 10k(和 100k)usingCallback
钩子并每 100 毫秒重新渲染一次。好像useCallback
的个数真的很多的时候才会有效果。查看下面的结果。
具有 10k 个钩子的函数组件:
每次渲染耗时8~12ms。
具有 100k 个钩子的函数组件:
每次渲染耗时25~80ms。
Class 具有 10k 个方法的组件:
每次渲染耗时4~5ms。
Class 组件有 100k 个方法:
每次渲染耗时4~6ms。
I've tested with 1k example too. But the profile result looks almost same as the one with 10k.
因此,当我的组件使用 100k 钩子时,我的浏览器中的惩罚很明显,而 class 组件没有显示出明显的差异。所以我想只要您没有使用超过 10k 钩子的组件,它就应该没问题。不过,该数量可能取决于客户端的运行时资源。
测试组件代码:
import React, { useState, useCallback, useEffect } from 'react';
const callbackCount = 10000
const useCrazyCounter = () => {
const callbacks = []
const [count, setCount] = useState(0)
for (let i = 1; i < callbackCount + 1; i++) {
// eslint-disable-next-line
callbacks.push(useCallback(() => {
setCount(prev => prev + i)
// eslint-disable-next-line
}, []))
}
return [count, ...callbacks]
}
const Counter = () => {
const [count, plusOne] = useCrazyCounter()
useEffect(() => {
const timer = setInterval(plusOne, 100)
return () => {
clearInterval(timer)
}}
, [])
return <div><div>{count}</div><div><button onClick={plusOne}>Plus One</button></div></div>
}
class ClassCounter extends React.Component {
constructor() {
super()
this.state = {
count: 0
}
for (let i = 1; i < callbackCount; i++) {
this['plus'+i] = () => {
this.setState(prev => ({
count: prev.count + i
}))
}
}
}
componentDidMount() {
this.timer = setInterval(() => {
this.plus1()
}, 100)
}
componentWillUnmount() {
clearInterval(this.timer)
}
render () {
return <div><div>{this.state.count}</div><div><button onClick={this.plus1}>Plus One</button></div></div>
}
}
const App = () => {
return (
<div className="App">
<Counter/>
{/* <ClassCounter/> */}
</div>
);
}
export default App;