【JS】再也不烦恼react性能了

再也不烦恼react性能了

石坚发布于 31 分钟前

【JS】再也不烦恼react性能了

困惑

写过react项目的同学,都经历过react性能优化,一般的方法是缓存某些计算,或者使用purecomponent、memo等方法减少不必要的组件渲染,以及使用context替代props透传等,下面探讨一种能从开始开发项目时就能保证页面性能的方法。

常用弊端方法

1.props透传

大多数的项目堆砌react代码时,都采用props一层层传递的方式,拼凑出一大堆组件代码,等到发现性能问题时再去查找瓶颈的原因。
【JS】再也不烦恼react性能了

任意位置父节点的渲染都会导致子孙节点的渲染,这些渲染有必要的也有非必要的,非必要的渲染就是导致性能危机的地方。
叶子节点的渲染可能依赖于某个props,这就需要props传递不被阻断,性能很难优化。

2.pureComponent、shouldComponentUpdate

这两种方式可以用来对比props的变化,即使祖先渲染了而自己的props非改变时就不渲染,节省性能,但这样也带来了其他的问题

2.1diff性能损失

这两种方式是通过diff比较来判断props是否变化的,而有些情况下由于写法等原因,props是必然变化的,就导致了盲目的使用不仅没有带来性能提升,反而多出了一部分diff计算时间

2.2叶子节点

如果用在某个父元素上,即使这个父元素不需要渲染,但是后面的子孙节点需要基于某个props来改变渲染,所以也必然要通过这个父元素的重新渲染来达到目的

3.memo

使用hooks的同学只能通过memo方法来判断组件是否需要渲染,使用memo在复杂场景下需要第二个参数的判断,弊端与上面类似,而且还会遇到一些该渲染而未渲染的坑点

4.useCallback、useMemo

配合memo等diff计算非必要渲染的手段,将props缓存起来,就是上面提到的pureComponent在有些情况下性能会变的更糟,原因就是在写法上没有缓存的话,就会浪费diff计算时间,例如

<Input onChange={e=>{setValue(e.target.value)}} />

由于onchange的属性是个匿名函数,每次组件渲染时,input传入的props都会变,就会导致memo、pureComponent等优化失效。所以请将传入组件的props使用useCallback或useMemo包裹起来,让props非必要不改变。
但是在antd组件中,没有使用memo等包裹组件,即使传入antd的props使用了useCallback、useMemo,也不能带来性能提升。
但是我们自己开发时封装的组件是需要做性能优化的,在传入这类组件的props时,需要使用useCallback或useMemo包装。

5.context

context方法是比较简单的性能优化策略,大量减少props的透传,再配合useContext的使用,写法变得非常简洁。
但是context也没法做到精确控制渲染的必要性,因为组件订阅了context后只要context中某个值发生变化,即使没有使用这个值也会导致组件重新渲染。
【JS】再也不烦恼react性能了

6.解决方法

6.1使用rxjs精确控制组件渲染时机

先不了解rxjs具体的概念,把rxjs当做是eventEmitters订阅工具,先手写两个方法eventInput(输入)、eventOutput(输出),并且把这两个方法放到context中,所有想要订阅eventOutput的组件,就使用useContext获取并且监听eventOutput事件,由于eventInput、eventOutput都是静态不变的函数,这就保证了context中的value不会变化(context不存放变化的value值),变化的始终是event中的流。达到的效果就是精确控制任意想变化的组件。
【JS】再也不烦恼react性能了
上图中,子孙节点也能控制任意位置的祖先节点的渲染,而不改变其他组件的渲染

6.2memo包裹

父组件重新渲染必然导致后续子组件的渲染,所以使用React.memo包裹每个组件。

这样就保证了可以放心使用memo,而不担心是否有多余的diff和未知的坑点。所有组件没有props传递,只关心自己订阅的值,只有自己订阅的值改变了才去渲染,做到手术刀式的控制渲染。

6.3副作用分离出ui

eventInput、eventOutput从输入到输出,中间是可以设定过程的,把ui中的所有副作用或计算都可以放到这些过程中去,既可以保证ui文件体积减小,也可以让关注点分离,ui只做跟渲染有关的事情。举个例子:

--a.tsx

eventInput({id:1})

--hooks.ts

eventOutput.pipe(({id})=>{

const res = await axios.get(id)

const colors = res.map(v=>v.color)

return colors

})

--b.tsx

eventOutput.sub(colors=>{

setState(colors)

})

7.活生生的例子

provider.jsx

import {useFetchResult,usePeriodChange} from 'useData.js'

export const context = React.createContext(({}))

const Provider = ({ children }) => {

const { fetchResultInput$,fetchResultOutput$ } = useFetchResult()

const { periodChangeInput$ } = usePeriodChange(fetchResultInput$)

return (

<context.Provider

value={{

fetchResultOutput$,

periodChangeInput$,

}}

>

{children}

</context.Provider>

)

}

export default Provider

useData.js

export const usePeriodChange = (fetchResultInput$) => {

const {periodChangeInput$} = useMemo(() => {

const periodChangeInput$ = new Subject()

return { periodChangeInput$ }

}, [])

useEffect(()=>{

const sub = periodChangeInput$.subscribe(period=>{

const params = {...period,appid:123}

fetchResultInput$.next(params)

})

return ()=>sub()

},[fetchResultInput$,periodChangeInput$])

return {periodChangeInput$}

}

export const useFetchResult = (fetchResultInput$) => {

return useMemo(() => {

const fetchResultInput$ = new Subject()

const fetchResultOutput$ = fetchResultInput$.pipe(

switchMap(params=>{

return axios.get('/',params)

}),

map(res=>{

...

calc(res)

...

return result

})

)

return { fetchResultInput$, fetchResultOutput$ }

}, [])

}

index.jsx

import Provider from './provider.jsx'

export default () => (

<Provider>

<Panel />

</Provider>

)

panel.jsx

export default () => (

<>

<Period />

<Table />

</>

)

preiod.jsx

export default () => {

const {periodChangeInput$} = useContext(context)

const onChange = useCallback((period)=>{

periodChangeInput$.next(period)

},[periodChangeInput$])

return <Select onChange={onChange} />

}

table.jsx

export default () => {

const [data,setData] = useState([])

const {fetchResultOutput$} = useContext(context)

useEffect(()=>{

const sub = fetchResultOutput$.subscribe(data=>setData(data))

return ()=>sub()

},[fetchResultOutput$])

return <Table data={data} />

}

html5javascript前端react.jsvue.js

阅读 19更新于 30 分钟前

本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议

avatar

石坚

326 声望

6 粉丝

0 条评论

得票时间

avatar

石坚

326 声望

6 粉丝

宣传栏

【JS】再也不烦恼react性能了

困惑

写过react项目的同学,都经历过react性能优化,一般的方法是缓存某些计算,或者使用purecomponent、memo等方法减少不必要的组件渲染,以及使用context替代props透传等,下面探讨一种能从开始开发项目时就能保证页面性能的方法。

常用弊端方法

1.props透传

大多数的项目堆砌react代码时,都采用props一层层传递的方式,拼凑出一大堆组件代码,等到发现性能问题时再去查找瓶颈的原因。
【JS】再也不烦恼react性能了

任意位置父节点的渲染都会导致子孙节点的渲染,这些渲染有必要的也有非必要的,非必要的渲染就是导致性能危机的地方。
叶子节点的渲染可能依赖于某个props,这就需要props传递不被阻断,性能很难优化。

2.pureComponent、shouldComponentUpdate

这两种方式可以用来对比props的变化,即使祖先渲染了而自己的props非改变时就不渲染,节省性能,但这样也带来了其他的问题

2.1diff性能损失

这两种方式是通过diff比较来判断props是否变化的,而有些情况下由于写法等原因,props是必然变化的,就导致了盲目的使用不仅没有带来性能提升,反而多出了一部分diff计算时间

2.2叶子节点

如果用在某个父元素上,即使这个父元素不需要渲染,但是后面的子孙节点需要基于某个props来改变渲染,所以也必然要通过这个父元素的重新渲染来达到目的

3.memo

使用hooks的同学只能通过memo方法来判断组件是否需要渲染,使用memo在复杂场景下需要第二个参数的判断,弊端与上面类似,而且还会遇到一些该渲染而未渲染的坑点

4.useCallback、useMemo

配合memo等diff计算非必要渲染的手段,将props缓存起来,就是上面提到的pureComponent在有些情况下性能会变的更糟,原因就是在写法上没有缓存的话,就会浪费diff计算时间,例如

<Input onChange={e=>{setValue(e.target.value)}} />

由于onchange的属性是个匿名函数,每次组件渲染时,input传入的props都会变,就会导致memo、pureComponent等优化失效。所以请将传入组件的props使用useCallback或useMemo包裹起来,让props非必要不改变。
但是在antd组件中,没有使用memo等包裹组件,即使传入antd的props使用了useCallback、useMemo,也不能带来性能提升。
但是我们自己开发时封装的组件是需要做性能优化的,在传入这类组件的props时,需要使用useCallback或useMemo包装。

5.context

context方法是比较简单的性能优化策略,大量减少props的透传,再配合useContext的使用,写法变得非常简洁。
但是context也没法做到精确控制渲染的必要性,因为组件订阅了context后只要context中某个值发生变化,即使没有使用这个值也会导致组件重新渲染。
【JS】再也不烦恼react性能了

6.解决方法

6.1使用rxjs精确控制组件渲染时机

先不了解rxjs具体的概念,把rxjs当做是eventEmitters订阅工具,先手写两个方法eventInput(输入)、eventOutput(输出),并且把这两个方法放到context中,所有想要订阅eventOutput的组件,就使用useContext获取并且监听eventOutput事件,由于eventInput、eventOutput都是静态不变的函数,这就保证了context中的value不会变化(context不存放变化的value值),变化的始终是event中的流。达到的效果就是精确控制任意想变化的组件。
【JS】再也不烦恼react性能了
上图中,子孙节点也能控制任意位置的祖先节点的渲染,而不改变其他组件的渲染

6.2memo包裹

父组件重新渲染必然导致后续子组件的渲染,所以使用React.memo包裹每个组件。

这样就保证了可以放心使用memo,而不担心是否有多余的diff和未知的坑点。所有组件没有props传递,只关心自己订阅的值,只有自己订阅的值改变了才去渲染,做到手术刀式的控制渲染。

6.3副作用分离出ui

eventInput、eventOutput从输入到输出,中间是可以设定过程的,把ui中的所有副作用或计算都可以放到这些过程中去,既可以保证ui文件体积减小,也可以让关注点分离,ui只做跟渲染有关的事情。举个例子:

--a.tsx

eventInput({id:1})

--hooks.ts

eventOutput.pipe(({id})=>{

const res = await axios.get(id)

const colors = res.map(v=>v.color)

return colors

})

--b.tsx

eventOutput.sub(colors=>{

setState(colors)

})

7.活生生的例子

provider.jsx

import {useFetchResult,usePeriodChange} from 'useData.js'

export const context = React.createContext(({}))

const Provider = ({ children }) => {

const { fetchResultInput$,fetchResultOutput$ } = useFetchResult()

const { periodChangeInput$ } = usePeriodChange(fetchResultInput$)

return (

<context.Provider

value={{

fetchResultOutput$,

periodChangeInput$,

}}

>

{children}

</context.Provider>

)

}

export default Provider

useData.js

export const usePeriodChange = (fetchResultInput$) => {

const {periodChangeInput$} = useMemo(() => {

const periodChangeInput$ = new Subject()

return { periodChangeInput$ }

}, [])

useEffect(()=>{

const sub = periodChangeInput$.subscribe(period=>{

const params = {...period,appid:123}

fetchResultInput$.next(params)

})

return ()=>sub()

},[fetchResultInput$,periodChangeInput$])

return {periodChangeInput$}

}

export const useFetchResult = (fetchResultInput$) => {

return useMemo(() => {

const fetchResultInput$ = new Subject()

const fetchResultOutput$ = fetchResultInput$.pipe(

switchMap(params=>{

return axios.get('/',params)

}),

map(res=>{

...

calc(res)

...

return result

})

)

return { fetchResultInput$, fetchResultOutput$ }

}, [])

}

index.jsx

import Provider from './provider.jsx'

export default () => (

<Provider>

<Panel />

</Provider>

)

panel.jsx

export default () => (

<>

<Period />

<Table />

</>

)

preiod.jsx

export default () => {

const {periodChangeInput$} = useContext(context)

const onChange = useCallback((period)=>{

periodChangeInput$.next(period)

},[periodChangeInput$])

return <Select onChange={onChange} />

}

table.jsx

export default () => {

const [data,setData] = useState([])

const {fetchResultOutput$} = useContext(context)

useEffect(()=>{

const sub = fetchResultOutput$.subscribe(data=>setData(data))

return ()=>sub()

},[fetchResultOutput$])

return <Table data={data} />

}

以上是 【JS】再也不烦恼react性能了 的全部内容, 来源链接: utcz.com/a/113553.html

回到顶部