React 性能优化完全指南,将自己这几年的心血总结成篇
本人分享一下这次字节跳动、美团、头条等大厂的面试真题涉及到的知识点,以及我个人的学习方法、学习路线等,当然也整理了一些学习文档资料出来是给大家的。知识点涉及比较全面,包括但不限于前端基础,HTML,CSS,JavaScript,Vue,ES6,HTTP,浏览器,算法等等CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】前端视频资料:一个人可以走的很快,
React 推荐将公共数据放在所有「需要该状态的组件」的公共祖先上,但将状态放在公共祖先上后,该状态就需要层层向下传递,直到传递给使用该状态的组件为止。
每次状态的更新都会涉及中间组件的 Render 过程,但中间组件并不关心该状态,它的 Render 过程只负责将该状态再传给子组件。在这种场景下可以将状态用发布者订阅者模式维护,只有关心该状态的组件才去订阅该状态,不再需要中间组件传递该状态。
当状态更新时,发布者发布数据更新消息,只有订阅者组件才会触发 Render 过程,中间组件不再执行 Render 过程。
只要是发布者订阅者模式的库,都可以进行该优化。比如:redux、use-global-state、React.createContext 等。例子参考:发布者订阅者模式跳过中间组件的渲染阶段[23],本示例使用 React.createContext 进行实现。
import { useState, useEffect, createContext, useContext } from ‘react’
const renderCntMap = {}
const renderOnce = name => {
return (renderCntMap[name] = (renderCntMap[name] || 0) + 1)
}
// 将需要公共访问的部分移动到 Context 中进行优化
// Context.Provider 就是发布者
// Context.Consumer 就是消费者
const ValueCtx = createContext()
const CtxContainer = ({ children }) => {
const [cnt, setCnt] = useState(0)
useEffect(
() => {
const timer = window.setInterval(() => {
setCnt(v => v + 1)
}, 1000)
return () => clearInterval(timer)
},
[setCnt]
)
return <ValueCtx.Provider value={cnt}>{children}</ValueCtx.Provider>
}
function CompA({}) {
const cnt = useContext(ValueCtx)
// 组件内使用 cnt
return (
组件 CompA Render 次数:
{renderOnce(‘CompA’)}
)
}
function CompB({}) {
const cnt = useContext(ValueCtx)
// 组件内使用 cnt
return (
组件 CompB Render 次数:
{renderOnce(‘CompB’)}
)
}
function CompC({}) {
return (
组件 CompC Render 次数:
{renderOnce(‘CompC’)}
)
}
export const PubSubCommunicate = () => {
return (
优化后场景
将状态提升至最低公共祖先的上层,用 CtxContainer 将其内容包裹。
每次 Render 时,只有组件A和组件B会重新 Render 。
父组件 Render 次数:
{renderOnce(‘parent’)}
)
}
export default PubSubCommunicate
运行后效果:TODO: 放图。从图中可看出,优化后只有使用了公共状态的组件 CompA 和 CompB 发生了更新,减少了父组件和 CompC 组件的 Render 次数。
useMemo 返回虚拟 DOM 可跳过该组件 Render 过程
利用 useMemo 可以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。
该方式与 React.memo 类似,但与 React.memo 相比有以下优势:
-
更方便。React.memo 需要对组件进行一次包装,生成新的组件。而 useMemo 只需在存在性能瓶颈的地方使用,不用修改组件。
-
更灵活。useMemo 不用考虑组件的所有 Props,而只需考虑当前场景中用到的值,也可使用 useDeepCompareMemo[24] 对用到的值进行深比较。
例子参考:useMemo 跳过组件 Render 过程[25]。
该例子中,父组件状态更新后,不使用 useMemo 的子组件会执行 Render 过程,而使用 useMemo 的子组件不会执行。
import { useEffect, useMemo, useState } from ‘react’
import ‘./styles.css’
const renderCntMap = {}
function Comp({ name }) {
renderCntMap[name] = (renderCntMap[name] || 0) + 1
return (
组件「
{name}」 Render 次数:
{renderCntMap[name]}
)
}
export default function App() {
const setCnt = useState(0)[1]
useEffect(
() => {
const timer = window.setInterval(() => {
setCnt(v => v + 1)
}, 1000)
return () => clearInterval(timer)
},
[setCnt]
)
const comp = useMemo(() => {
return
}, [])
return (
{comp}
)
}
debounce、throttle 优化频繁触发的回调
在搜索组件中,当 input 中内容修改时就触发搜索回调。当组件能很快处理搜索结果时,用户不会感觉到输入延迟。
但实际场景中,中后台应用的列表页非常复杂,组件对搜索结果的 Render 会造成页面卡顿,明显影响到用户的输入体验。
在搜索场景中一般使用 useDebounce[26] + useEffect 的方式获取数据。
例子参考:debounce-search[27]。
import { useState, useEffect } from ‘react’
import { useDebounce } from ‘use-debounce’
export default function App() {
const [text, setText] = useState(‘Hello’)
const [debouncedValue] = useDebounce(text, 300)
useEffect(
() => {
// 根据 debouncedValue 进行搜索
},
[debouncedValue]
)
return (
<input
defaultValue={‘Hello’}
onChange={e => {
setText(e.target.value)
}}
/>
Actual value: {text}
Debounce value: {debouncedValue}
)
}
为什么搜索场景中是使用 debounce,而不是 throttle 呢?throttle 是 debounce 的特殊场景,throttle 给 debounce 传了 maxWait 参数,可参考 useThrottleCallback[28]。
在搜索场景中,只需响应用户最后一次输入,无需响应用户的中间输入值,debounce 更适合使用在该场景中。
而 throttle 更适合需要实时响应用户的场景中更适合,如通过拖拽调整尺寸或通过拖拽进行放大缩小(如:window 的 resize 事件)。
实时响应用户操作场景中,如果回调耗时小,甚至可以用 requestAnimationFrame 代替 throttle。
懒加载
在 SPA 中,懒加载优化一般用于从一个路由跳转到另一个路由。
还可用于用户操作后才展示的复杂组件,比如点击按钮后展示的弹窗模块(有时候弹窗就是一个复杂页面 ???)。
在这些场景下,结合 Code Split 收益较高。懒加载的实现是通过 Webpack 的动态导入和 React.lazy
方法,参考例子 lazy-loading[29]。
实现懒加载优化时,不仅要考虑加载态,还需要对加载失败进行容错处理。
import { lazy, Suspense, Component } from ‘react’
import ‘./styles.css’
// 对加载失败进行容错处理
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
return { hasError: true }
}
render() {
if (this.state.hasError) {
return
这里处理出错场景
}
return this.props.children
}
}
const Comp = lazy(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
reject(new Error(‘模拟网络出错’))
} else {
resolve(import(‘./Component’))
}
}, 2000)
})
})
export default function App() {
return (
实现懒加载优化时,不仅要考虑加载态,还需要对加载失败进行容错处理。
)
}
懒渲染
懒渲染指当组件进入或即将进入可视区域时才渲染组件。常见的组件 Modal/Drawer 等,当 visible 属性为 true 时才渲染组件内容,也可以认为是懒渲染的一种实现。懒渲染的使用场景有:
-
页面中出现多次的组件,且组件渲染费时、或者组件中含有接口请求。如果渲染多个带有请求的组件,由于浏览器限制了同域名下并发请求的数量,就可能会阻塞可见区域内的其他组件中的请求,导致可见区域的内容被延迟展示。
-
需用户操作后才展示的组件。这点和懒加载一样,但懒渲染不用动态加载模块,不用考虑加载态和加载失败的兜底处理,实现上更简单。
懒渲染的实现中判断组件是否出现在可视区域内是通过 react-visibility-observer[30] 进行监听。例子参考:懒渲染[31]
import { useState, useEffect } from ‘react’
import VisibilityObserver, {
useVisibilityObserver
} from ‘react-visibility-observer’
const VisibilityObserverChildren = ({ callback, children }) => {
const { isVisible } = useVisibilityObserver()
useEffect(
() => {
callback(isVisible)
},
[callback, isVisible]
)
return <>{children}</>
}
export const LazyRender = () => {
const [isRendered, setIsRendered] = useState(false)
if (!isRendered) {
return (
<VisibilityObserver rootMargin={‘0px 0px 0px 0px’}>
<VisibilityObserverChildren
callback={isVisible => {
if (isVisible) {
setIsRendered(true)
}
}}
)
}
console.log(‘滚动到可视区域才渲染’)
return
}
export default LazyRender
虚拟列表
虚拟列表是懒渲染的一种特殊场景。虚拟列表的组件有 react-window[32] 和 react-virtualized,它们都是同一个作者开发的。
react-window 是 react-virtualized 的轻量版本,其 API 和文档更加友好。
所以新项目中推荐使用 react-window,而不是使用 Star 更多的 react-virtualized。
使用 react-window 很简单,只需要计算每项的高度即可。下面代码中每一项的高度是 35px。
例子参考:官方示例[33]
import { FixedSizeList as List } from ‘react-window’
const Row = ({ index, style }) =>
const Example = () => (
<List
height={150}
itemCount={1000}
itemSize={35} // 每项的高度为 35
width={300}
{Row}
)
如果每项的高度是变化的,可给 itemSize 参数传一个函数。对于这个优化点,笔者遇到一个真实案例。
在公司的招聘项目中,通过下拉菜单可查看某个候选人的所有投递记录。平常这个列表也就几十条,但后来用户反馈『下拉菜单点击后要很久才能展示出投递列表』。
该问题的原因就是这个候选人在我们系统中有上千条投递,一次性展示上千条投递导致页面卡住了。
所以在开发过程中,遇到接口返回的是所有数据时,需提前预防这类 bug,使用虚拟列表优化。
跳过回调函数改变触发的 Render 过程
React 组件的 Props 可以分为两类。
a) 一类是在对组件 Render 有影响的属性,如:页面数据、getPopupContainer[34] 和 renderProps 函数。
b) 另一类是组件 Render 后的回调函数,如:onClick、onVisibleChange[35]。
b) 类属性并不参与到组件的 Render 过程,因为可以对 b) 类属性进行优化。
当 b)类属性发生改变时,不触发组件的重新 Render ,而是在回调触发时调用最新的回调函数。
Dan Abramov 在 A Complete Guide to useEffect[36] 文章中认为,每次 Render 都有自己的事件回调是一件很酷的特性。
但该特性要求每次回调函数改变就触发组件的重新 Render ,这在性能优化过程中是可以取舍的。
例子参考:跳过回调函数改变触发的 Render 过程[37]。
以下代码比较难以理解,可通过调试该例子,帮助理解消化。
import { Children, cloneElement, memo, useEffect, useRef } from ‘react’
import { useDeepCompareMemo } from ‘use-deep-compare’
import omit from ‘lodash.omit’
let renderCnt = 0
export function SkipNotRenderProps({ children, skips }) {
if (!skips) {
// 默认跳过所有回调函数
skips = prop => prop.startsWith(‘on’)
}
const child = Children.only(children)
const childProps = child.props
const propsRef = useRef({})
const nextSkippedPropsRef = useRef({})
Object.keys(childProps)
.filter(it => skips(it))
.forEach(key => {
// 代理函数只会生成一次,其值始终不变
nextSkippedPropsRef.current[key] =
nextSkippedPropsRef.current[key] ||
function skipNonRenderPropsProxy(…args) {
propsRef.current[key].apply(this, args)
}
})
useEffect(() => {
propsRef.current = childProps
})
// 这里使用 useMemo 优化技巧
// 除去回调函数,其他属性改变生成新的 React.Element
return useDeepCompareMemo(
() => {
return cloneElement(child, {
…child.props,
…nextSkippedPropsRef.current
})
},
[omit(childProps, Object.keys(nextSkippedPropsRef.current))]
)
}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
最后
本人分享一下这次字节跳动、美团、头条等大厂的面试真题涉及到的知识点,以及我个人的学习方法、学习路线等,当然也整理了一些学习文档资料出来是给大家的。知识点涉及比较全面,包括但不限于前端基础,HTML,CSS,JavaScript,Vue,ES6,HTTP,浏览器,算法等等
前端视频资料:
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-0g5LmE8o-1712263199800)]
最后
本人分享一下这次字节跳动、美团、头条等大厂的面试真题涉及到的知识点,以及我个人的学习方法、学习路线等,当然也整理了一些学习文档资料出来是给大家的。知识点涉及比较全面,包括但不限于前端基础,HTML,CSS,JavaScript,Vue,ES6,HTTP,浏览器,算法等等
[外链图片转存中…(img-RceawoTp-1712263199800)]
前端视频资料:
[外链图片转存中…(img-i0AnI65a-1712263199801)]
一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!
AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算
更多推荐
所有评论(0)