字节跳动前端算法面试题,这可能是全网最全的react性能优化知识锦集,一起学前端

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 时才渲染组件内容,也可以认为是懒渲染的一种实现。

懒渲染的使用场景有:

  1. 页面中出现多次的组件,且组件渲染费时、或者组件中含有接口请求。如果渲染多个带有请求的组件,由于浏览器限制了同域名下并发请求的数量,就可能会阻塞可见区域内的其他组件中的请求,导致可见区域的内容被延迟展示。

  2. 需用户操作后才展示的组件。这点和懒加载一样,但懒渲染不用动态加载模块,不用考虑加载态和加载失败的兜底处理,实现上更简单。

懒渲染的实现中判断组件是否出现在可视区域内是通过 react-visibility-observer[31] 进行监听。

例子参考:懒渲染[32]

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 

我是 LazyRender 组件

}

export default LazyRender

复制代码

虚拟列表

虚拟列表是懒渲染的一种特殊场景。虚拟列表的组件有 react-window[33] 和 react-virtualized,它们都是同一个作者开发的。react-window 是 react-virtualized 的轻量版本,其 API 和文档更加友好。所以新项目中推荐使用 react-window,而不是使用 Star 更多的 react-virtualized。

使用 react-window 很简单,只需要计算每项的高度即可。下面代码中每一项的高度是 35px。

例子参考:官方示例[34]

import { FixedSizeList as List } from “react-window”

const Row = ({ index, style }) => 

Row {index}

const Example = () => (

<List

height={150}

itemCount={1000}

itemSize={35} // 每项的高度为 35

width={300}

{Row}

)

复制代码

如果每项的高度是变化的,可给 itemSize 参数传一个函数。

对于这个优化点,笔者遇到一个真实案例。在公司的招聘项目中,通过下拉菜单可查看某个候选人的所有投递记录。平常这个列表也就几十条,但后来用户反馈『下拉菜单点击后要很久才能展示出投递列表』。该问题的原因就是这个候选人在我们系统中有上千条投递,一次性展示上千条投递导致页面卡住了。所以在开发过程中,遇到接口返回的是所有数据时,需提前预防这类 bug,使用虚拟列表优化。

2. 批量更新

我们先回忆一道前几年的 React 面试常考题,React 类组件中 setState 是同步的还是异步的?如果对类组件不熟悉也没关系,可以将 setState 理解为 useState 的第二个返回值。

balabala…

答案是:在 React 管理的事件回调和生命周期中,setState 是异步的,而其他时候 setState 都是同步的。这个问题根本原因就是 React 在自己管理的事件回调和生命周期中,对于 setState 是批量更新的,而在其他时候是立即更新的。读者可参考线上示例 setState 同步还是异步[35],并自行验证。

批量更新 setState 时,多次执行 setState 只会触发一次 Render 过程。相反在立即更新 setState 时,每次 setState 都会触发一次 Render 过程,就存在性能影响。

假设有如下组件代码,该组件在 getData() 的 API 请求结果返回后,分别更新了两个 State 。线上代码实操参考:batchUpdates 批量更新[36]。

function NormalComponent() {

const [list, setList] = useState(null)

const [info, setInfo] = useState(null)

useEffect(() => {

;(async () => {

const data = await getData()

setList(data.list)

setInfo(data.info)

})()

}, [])

return 

非批量更新组件时 Render 次数:{renderOnce(“normal”)}

}

复制代码

该组件会在 setList(data.list) 后触发组件的 Render 过程,然后在 setInfo(data.info)后再次触发 Render 过程,造成性能损失。遇到该问题,开发者有两种实现批量更新的方式来解决该问题:

将多个 State 合并为单个 State。例如使用 const [data, setData] = useState({ list: null, info: null }) 替代 list 和 info 两个 State。

使用 React 官方提供的 unstable_batchedUpdates 方法,将多次 setState 封装到 unstable_batchedUpdates 回调中。修改后代码如下。

function BatchedComponent() {

const [list, setList] = useState(null)

const [info, setInfo] = useState(null)

useEffect(() => {

;(async () => {

const data = await getData()

unstable_batchedUpdates(() => {

setList(data.list)

setInfo(data.info)

})

})()

}, [])

return 

批量更新组件时 Render 次数:{renderOnce(“batched”)}

}

复制代码

拓展知识

  1. 推荐阅读为什么 setState 是异步的?[37]
  1. 为什么面试官不会问“函数组件中的 setState 是同步的还是异步的?”?因为函数组件中生成的函数是通过闭包引用了 state,而不是通过 this.state 的方式引用 state,所以函数组件的处理函数中 state 一定是旧值,不可能是新值。可以说函数组件已经将这个问题屏蔽掉了,所以面试官也就不会问了。可参考线上示例[38]。
  1. 根据官方文档[39],在 React 并发模式中,将默认以批量更新方式执行 setState。到那时候,也可能就不需要这个优化了。

3. 按优先级更新,及时响应用户

优先级更新是批量更新的逆向操作,其思想是:优先响应用户行为,再完成耗时操作。

常见的场景是:页面弹出一个 Modal,当用户点击 Modal 中的确定按钮后,代码将执行两个操作。a) 关闭 Modal。b) 页面处理 Modal 传回的数据并展示给用户。当 b) 操作需要执行 500ms 时,用户会明显感觉到从点击按钮到 Modal 被关闭之间的延迟。

例子参考:CodeSandbox 在线 Demo[40]。在该例子中,用户添加一个整数后,页面要隐藏输入框,并将新添加的整数加入到整数列表,将列表排序后再展示。以下为一般的实现方式,将 slowHandle 函数作为用户点击按钮的回调函数。

const slowHandle = () => {

setShowInput(false)

setNumbers([…numbers, +inputValue].sort((a, b) => a - b))

}

复制代码

slowHandle() 执行过程耗时长,用户点击按钮后会明显感觉到页面卡顿。如果让页面优先隐藏输入框,用户便能立刻感知到页面更新,不会有卡顿感。实现优先级更新的要点是将耗时任务移动到下一个宏任务中执行,优先响应用户行为。 例如在该例中,将 setNumbers 移动到 setTimeout 的回调中,用户点击按钮后便能立即看到输入框被隐藏,不会感知到页面卡顿。优化后的代码如下。

const fastHandle = () => {

// 优先响应用户行为

setShowInput(false)

// 将耗时任务移动到下一个宏任务执行

setTimeout(() => {

setNumbers([…numbers, +inputValue].sort((a, b) => a - b))

})

}

复制代码

4. 缓存优化

缓存优化往往是最简单有效的优化方式,在 React 组件中常用 useMemo 缓存上次计算的结果。当 useMemo 的依赖未发生改变时,就不会触发重新计算。一般用在「计算派生状态的代码」非常耗时的场景中,如:遍历大列表做统计信息。

拓展知识

  1. React 官方并不保证 useMemo 一定会进行缓存,所以可能在依赖不改变时,仍然执行重新计算。参考 How to memoize calculations[41]
  1. useMemo 只能缓存最近一次函数执行的结果,如果想缓存更多次函数执行的结果,可使用memoizee[42]。

5. debounce、throttle 优化频繁触发的回调

在搜索组件中,当 input 中内容修改时就触发搜索回调。当组件能很快处理搜索结果时,用户不会感觉到输入延迟。但实际场景中,中后台应用的列表页非常复杂,组件对搜索结果的 Render 会造成页面卡顿,明显影响到用户的输入体验。

在搜索场景中一般使用 useDebounce[43] + useEffect 的方式获取数据。

例子参考:debounce-search[44]。

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[45]。在搜索场景中,只需响应用户最后一次输入,无需响应用户的中间输入值,debounce 更适合使用在该场景中。而 throttle 更适合需要实时响应用户的场景中更适合,如通过拖拽调整尺寸或通过拖拽进行放大缩小(如:window 的 resize 事件)。实时响应用户操作场景中,如果回调耗时小,甚至可以用 requestAnimationFrame 代替 throttle。

React Profiler 定位 Render 过程瓶颈

=============================

React Profiler 是 React 官方提供的性能审查工具,本文只介绍笔者的使用心得,详细的使用手册请移步官网文档[46]。

Profiler 只记录了 Render 过程耗时


通过 React Profiler,开发者可以查看组件 Render 过程耗时,但无法知晓提交阶段的耗时。尽管 Profiler 面板中有 Committed at 字段,但这个字段是相对于录制开始时间,根本没有意义。所以提醒读者不要通过 Profiler 定位非 Render 过程的性能瓶颈问题

通过在 React v16 版本上进行实验,同时开启 Chrome 的 Performance 和 React Profiler 统计。如下图,在 Performance 面板中,调和阶段和提交阶段耗时分别为 642ms 和 300ms,而 Profiler 面板中只显示了 642ms,没有提交阶段的耗时信息。

拓展知识

  1. React 在 v17 版本后已移除 User Timing 统计功能,具体原因可参考 PR#18417[47]。
  1. 在 v17 版本上,笔者也通过测试代码[48]验证了 Profiler 中的统计信息并不包含提交阶段,有兴趣的读者可以看看。

开启「记录组件更新原因」


点击面板上的齿轮,然后勾选「Record why each component rendered while profiling.」,如下图。

然后点击面板中的虚拟 DOM 节点,右侧便会展示该组件重新 Render 的原因。

定位产生本次 Render 过程原因


由于 React 的批量更新(Batch Update)机制,产生一次 Render 过程可能涉及到很多个组件的状态更新。那么如何定位是哪些组件状态更新导致的呢?

在 Profiler 面板左侧的虚拟 DOM 树结构中,从上到下审查每个发生了渲染的(不会灰色的)组件。如果组件是由于 State 或 Hook 改变触发了 Render 过程,那它就是我们要找的组件,如下图。

结语

==

笔者是从年前开始写这篇文章,到发布时已经写了一个月了,期间断断续续将自己这几年对 React 的理解加入到文章中,然后调整措辞和丰富示例,最后终于在周四前完成(周四是我定的 deadline ???)。

既然自己付出了那么多努力,那就希望它能成为一份优秀的 React 优化手册吧。


如果您对文章有疑问或者有更多优化技巧,欢迎评论。

参考资料

[1]

列表项使用 key 属性: #heading-9

[2]

避免在 didMount、didUpdate 中更新组件 State: #heading-15

[3]

React 组件的生命周期图: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

[4]

介绍事件循环的视频: https://www.youtube.com/watch?v=u1kqx6AenYw&t=853s

[5]

展开语法: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax

[6]

…state, item]`。这点可参考 Dan Abramov 在[演讲 Redux 时: https://www.youtube.com/watch?v=xsSnOQynTHs&t=690s

[7]

发布者订阅者跳过中间组件 Render 过程: #heading-7

[8]

跳过回调函数改变触发的 Render 过程: #heading-11

[9]

React Server Hooks 代码: https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-dom/src/server/ReactPartialRendererHooks.js#L452

[10]

useCallback: https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-reconciler/src/ReactFiberHooks.new.js#L1590

[11]

useMemo: https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-reconciler/src/ReactFiberHooks.new.js#L1613

[12]

发布者订阅者模式跳过中间组件的渲染阶段: https://codesandbox.io/s/fabuzhedingyuezhemoshitiaoguozhongjianzujiande-render-guocheng-nm7nt?file=/src/PubSubCommunicate.js

[13]

before-you-memo: https://overreacted.io/before-you-memo/

[14]

React 官方推荐: https://reactjs.org/docs/lists-and-keys.html#keys

[15]

没有添加、删除、排序功能的分页列表: https://codesandbox.io/s/meiyoutianjiashanchupaixugongnengdefenyeliebiao-d6zqr?file=/src/App.js

[16]

useDeepCompareMemo: https://github.com/sandiiarov/use-deep-compare#usedeepcomparememo

[17]

useMemo 跳过组件 Render 过程: https://codesandbox.io/s/usememo-tiaoguozujian-render-guocheng-bzz9r

[18]

getPopupContainer: https://ant.design/components/dropdown/

[19]

onVisibleChange: https://ant.design/components/dropdown/

[20]

A Complete Guide to useEffect: https://overreacted.io/a-complete-guide-to-useeffect/#each-render-has-its-own-event-handlers

[21]

跳过回调函数改变触发的 Render 过程: https://codesandbox.io/s/tiaoguohuidiaohanshugaibianhongfade-render-guocheng-3i59n

[22]

Hooks 按需更新: https://codesandbox.io/s/hooks-anxugengxin-tinzp?file=/src/hooks.js:420-463

[23]

react-spring: https://github.com/pmndrs/react-spring

[24]

CodeSandbox 在线 Demo: https://codesandbox.io/s/donghuakuzhijiexiugai-domtiaoguoxuanranjieduan-ij7px

[25]

React 工作流: #heading-0

[26]

getDerivedStateFromProps: https://reactjs.org/docs/react-component.html#static-getderivedstatefromprops

[27]

函数调用时执行 setState: https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops

[28]

use-swr 的源码: https://github.com/vercel/swr/blob/0.3.8/src/use-swr.ts#L536

[29]

设置 isValidating 状态为 true: https://github.com/vercel/swr/blob/dedc017248e3de9502f5d9ff874d45de3b20ab06/src/use-swr.ts#L352

[30]

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

自学几个月前端,为什么感觉什么都没学到??


这种现象在很多的初学者和自学前端的同学中是比较的常见的。

因为自学走的弯路是比较的多的,会踩很多的坑,学习的过程中是比较的迷茫的。

最重要的是,在学习的过程中,不知道每个部分该学哪些知识点,学到什么程度才算好,学了能做什么。

很多自学的朋友往往都是自己去找资料学习的,资料上有的或许就学到了,资料上没有的或许就没有学到。

这就会给人一个错误的信息就是,我把资料上的学完了,估计也-就差不多的了。

但是真的是这样的吗?非也,因为很多人找的资料就是很基础的。学完了也就是掌握一点基础的东西。分享给你一份前端分析路线,你可以参考。

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

还有很多的同学在学习的过程中一味的追求学的速度,很快速的刷视频,写了后面忘了前面,最后什么都没有学到,什么都知道,但是什么都不懂,要具体说,也说不出个所以然。

所以学习编程一定要注重实践操作,练习敲代码的时间一定要多余看视频的时间。

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算


这种现象在很多的初学者和自学前端的同学中是比较的常见的。

因为自学走的弯路是比较的多的,会踩很多的坑,学习的过程中是比较的迷茫的。

最重要的是,在学习的过程中,不知道每个部分该学哪些知识点,学到什么程度才算好,学了能做什么。

很多自学的朋友往往都是自己去找资料学习的,资料上有的或许就学到了,资料上没有的或许就没有学到。

这就会给人一个错误的信息就是,我把资料上的学完了,估计也-就差不多的了。

但是真的是这样的吗?非也,因为很多人找的资料就是很基础的。学完了也就是掌握一点基础的东西。分享给你一份前端分析路线,你可以参考。

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

还有很多的同学在学习的过程中一味的追求学的速度,很快速的刷视频,写了后面忘了前面,最后什么都没有学到,什么都知道,但是什么都不懂,要具体说,也说不出个所以然。

所以学习编程一定要注重实践操作,练习敲代码的时间一定要多余看视频的时间。

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

  • 17
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值