React性能优化
# 分维度整理
# 渲染优化(React 层面)
- 减少不必要重渲染
◦ 组件:React.memo 纯组件浅对比 props;
◦ 函数:useCallback缓存回调传给子组件;
◦ 计算值:useMemo缓存复杂计算;
◦ 拆分组件,细粒度更新,避免顶层状态变更全树重渲染。
- 长列表优化
◦ 虚拟列表react-window / react-virtualized,只渲染可视区域DOM;
◦ 分页、懒加载,减少一次性渲染数据量。
- 副作用优化
◦ useEffect正确填写依赖,及时清理定时器、事件监听;
◦ 大量 DOM 操作放 useLayoutEffect,避免多次回流。
- 状态优化
◦ 拆分 useState,局部状态局部更新;
◦ 复杂状态用 useReducer,集中更新逻辑。
# 资源加载优化
静态资源:图片压缩、
webp格式、懒加载、CDN;代码分割:路由懒加载、动态
import,分包减小首屏包体积;缓存策略:浏览器强缓存、协商缓存;
第三方库按需引入,移除冗余依赖。
# 浏览器渲染层优化
减少
DOM操作,批量修改DOM;避免频繁回流重绘,使用
transform、opacity开启GPU加速;防抖节流处理高频事件(
scroll、resize、input)。
# 网络请求优化
请求合并、接口分页;
请求缓存
SWR/React Query,重复请求拦截;避免渲染中重复发请求。
# 其他性能问题
# React
React 为什么会重新渲染?React 的更新机制是什么?
答:React 组件重新渲染主要是由于组件自身 state 更新、父组件重新渲染导致 props 变化、Context 数据变化等原因触发。当 React 检测到组件需要更新时,会重新执行函数组件,生成新的 Virtual DOM,然后通过 Diff 算法比较新旧 Virtual DOM,最终只更新真实 DOM 中发生变化的部分。
state 修改一定会触发 render 吗?
答:不一定。如果调用 setState,但是新旧 state 值相同,React 会通过 Object.is 判断,如果值没有变化,可能会跳过重新渲染。
props 变化如何判断?
答:React 默认通过父组件重新渲染,然后重新比较传入组件的 props。如果 props 引用发生变化,React 会认为 props 更新。
React.memo 为什么可以减少渲染?
答:React.memo 会对组件的 props 进行浅比较,如果 props 没有变化,则复用之前的渲染结果,避免组件重新执行。
useMemo 一定能优化性能吗?
答:useMemo 本身也有缓存成本,并不是所有场景都适合使用。如果计算逻辑简单,使用 useMemo 反而增加比较成本。
React.memo 的原理是什么?什么时候不适合使用?
答:React.memo 本质是一个高阶组件,通过对比组件前后 props 是否变化决定是否重新渲染。如果 props 没变化,则跳过组件 render。
React.memo 和 PureComponent 区别?
答:两者作用类似,都是通过浅比较减少无效渲染。React.memo 用于函数组件;PureComponent 用于 class组件;React.memo 可以通过第二个参数自定义比较逻辑;
为什么 memo 之后还是会更新?
答:memo仅仅浅对比。props引用变化;state变化;context变化;父组件传入新的对象都会再更新。
props 是对象怎么办?
答:因为 memo 是浅比较,对象比较的是引用地址,所以如果对象每次重新创建,会导致重新渲染。可以使用 useMemo 固定对象引用,或者优化 props 结构。
const user = useMemo(()=>({ name }),[name])1
2
3useMemo 和 useCallback 的区别?
答:useMemo 缓存的是计算结果;useCallback 缓存的是函数引用。
两者解决什么问题?
答:能带来性能优化,减少不必要的渲染
是否所有函数都应该 useCallback?
答:不是,多数用来作为组件的函数,减少重复渲染
useMemo 缓存的数据什么时候释放?
答:React 会在组件卸载时释放缓存;另外 React 也可能根据内部策略丢弃缓存,所以不能依赖 useMemo 做业务数据存储。
React 为什么需要 key?
答:key 是 React Diff 算法识别列表元素身份的重要依据。通过 key,React 可以判断哪些节点新增、删除、移动,从而提高列表更新效率。
为什么不推荐 index 做 key?
答:当列表发生插入、删除、排序时,index 会发生变化,导致 React 错误复用组件状态。
Redux / Zustand 为什么可以做到跨组件通信?
答:Redux/Zustand 本质维护一个独立于 React 组件树的全局 store,任何组件都可以订阅 store 数据,当数据变化时通知对应组件更新。
Zustand 和 Context 区别?
答:Zustand 使用订阅机制,组件只监听自己需要的数据,不像 Context value 改变会导致所有消费者重新渲染。
为什么 Zustand 性能更好?
答:
Zustand 如何避免无效更新?
答:通过 selector 精确订阅状态,只监听需要的数据,并且内部通过 Object.is 判断数据是否变化。
大型项目如何设计状态管理?
答:以 项目为例,蓝牙状态、球队状态、脚环信息、脚环文件信息、用户管理
什么情况下使用 Context,什么情况下使用状态管理库?
答:像主题颜色使用Context,脚环状态会使用状态管理库
React Fiber 是什么?为什么需要 Fiber?
React Diff 算法原理?
Virtual DOM 一定比直接操作 DOM 快吗?
如何优化 React 首屏性能?
如何减少 React Bundle 体积?
React 为什么会出现内存泄漏?如何避免?
# React Native
React Native 为什么性能不如 Native?
答:React Native 并不是简单套壳,而是在 JS 层运行 React 逻辑,通过 JS 与 Native 通信调用原生能力。性能差异主要来自跨语言通信、JS线程和Native线程之间的数据传递,以及部分复杂 UI 需要经过额外转换。
RN 架构是什么?
答:旧架构
React Component ↓ JS Thread ↓ Bridge ↓ Native Module ↓ Android/iOS1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17新架构:
JS ↓ JSI ↓ TurboModule/Fabric ↓ Native1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19JS 和 Native 怎么通信?
答:使用native module建立一个桥梁,在原生的位置写方法暴露出来,然后在桥调用原生方法,再用promise方法往JS层暴露方法,从而达到通信
Bridge 有什么问题?
答:Bridge 是异步通信,需要序列化和反序列化数据,大量通信时会产生性能瓶颈。
RN 页面卡顿如何排查?
答:当页面有卡顿的现象,首先使用React DevTools Profiler去进行排查,看看每个组件的调用渲染次数
JS线程阻塞
原因:大量计算、JSON解析、高频setState
解决:优化计算、使用memo、移到Native
大量render
答:解决React.memo、useMemo、useCallback、状态拆分
动画执行
答:普通 Animated 如果运行在 JS Thread,会受到 JS 阻塞影响。可以使用 Reanimated,将动画逻辑放到 UI Thread 执行,提高流畅度。
图片加载
答:可以使用懒加载,图片压缩、缓存、CDN
Native耗时
答:检查 Native Module 是否存在耗时操作,例如蓝牙扫描、文件处理、大量数据解析,避免阻塞主线程,可以放到后台线程执行。
RN 为什么 FlatList 比 ScrollView 性能好?
答:FlatList 基于 VirtualizedList 实现,通过窗口化渲染,只维护当前可见区域和附近的数据,而ScrollView 会把所有数据线渲染出来
FlatList原理?
答:
VirtualizedList?
答:
如何优化 FlatList?
答:配置好渲染数量,每一个item的高度固定好,
RN 动画为什么推荐 Reanimated?
答:Reanimated 推荐使用是因为动画计算可以运行在 UI Thread,而不是 JS Thread。当 JS 线程阻塞时,普通动画可能卡顿,而 Reanimated 可以保持动画流畅。
React Native 的渲染流程是什么?
Native Module中的Promise和Callback区别?
Native EventEmitter原理?
TurboModule是什么?
什么是 JSI?
# App发布与Native方向
RN App启动流程是什么?
答:App启动时,Native 首先启动 Application,然后创建 React Native Runtime,加载 JS Bundle,初始化 ReactInstanceManager,创建 JS 与 Native 通信环境,最后渲染 React Component。
# BLE
BLE 为什么会断开?如何提高稳定性?
答:断开主要会有异常和正常,通过 manualDisconnect 标识判断主动断开。如果主动断开,设置 true,断开完成后释放资源并恢复默认状态。如果 disconnect 时标识为 false,则认为异常断开,进入重连流程。还有,APP退到后台的情况,可能实际断开了,但是状态还是记录着未断开,这种情况需要监听APP的生命周期,后台回到前台的时候,要先去判断一下蓝牙的状态再进行通信。还有要监听好设备的蓝牙开关和权限问题。
BLE 数据高频返回如何避免页面卡顿?
答:BLE 数据频率较高时,如果每次收到数据都 setState,会导致 React 高频 render。因此会使用 ref 或 buffer 暂存数据,根据一定时间间隔或者数据数量批量更新 UI。
BLE 50次/s ↓ buffer ↓ 500ms flush ↓ UI更新1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 部署
Vite开发环境为什么快?
Vite 开发环境快主要是因为利用浏览器原生 ESM 能力,不需要像 Webpack 一样启动时提前打包整个项目,而是按需加载模块。同时 Vite 使用 esbuild 进行依赖预构建,速度远快于传统 JavaScript 打包工具。
Vite生产环境为什么还需要Rollup?
Vite开发环境主要利用ESM实现快速启动,而生产环境仍然使用Rollup进行打包,因为生产环境更关注代码压缩、Tree Shaking、资源优化以及兼容性。
ESM是什么?
ESM是JavaScript官方模块化规范,通过import/export实现模块管理。相比CommonJS,它具有静态分析能力,可以被浏览器直接支持,同时方便Tree Shaking优化。
HMR怎么实现?
Hot Module Replacement 热模块替换
Vite通过监听文件变化,利用WebSocket通知浏览器模块更新,然后基于ESM重新请求变化模块,实现局部替换。React项目中结合Fast Refresh,可以做到代码更新但保留组件状态。
Vite和Webpack区别
Vite 和 Webpack 最大区别在于开发环境的构建方式。
Webpack 在开发启动时,会先分析整个项目依赖关系,然后进行打包生成 bundle,项目越大,启动时间越长。
Vite 利用浏览器原生 ESM 能力,开发环境不需要提前打包整个项目,而是启动 Dev Server,当浏览器请求某个模块时,再按需加载对应模块,因此启动速度更快。
同时 Vite 使用 esbuild 对依赖进行预构建,提高依赖处理速度。
在生产环境中,Vite 和 Webpack 都需要进行打包优化,Vite 通常使用 Rollup 完成生产构建。
Tree Shaking原理
Tree Shaking 是基于 ES Module 静态分析能力实现的代码消除技术,它会分析模块中哪些代码被引用,哪些代码没有被使用,然后在生产打包阶段删除未使用代码,从而减少最终 bundle 体积。
因为 CommonJS 是运行时加载,模块引用关系只有运行时才能确定,而 ESM 是静态结构,打包工具在编译阶段就可以分析依赖关系。
为什么ESM比CommonJS适合前端
ESM 是 JavaScript 官方模块规范,使用 import/export,并且模块依赖关系在编译阶段就可以确定,因此支持静态分析、Tree Shaking 和代码分割。同时浏览器原生支持 ESM,可以直接通过 script type="module" 加载。
HMR为什么能保留React状态
HMR 能保留 React 状态主要依赖 React Fast Refresh 机制。它不会刷新整个页面,而是替换发生变化的组件模块,并尝试保留组件实例和 Hooks 状态。
流程:
修改组件代码 ↓ HMR通知浏览器 ↓ React Fast Refresh判断组件是否可以热替换 ↓ 保留Hooks状态 ↓ 重新执行组件1
2
3
4
5
6
7
8
9如果组件结构变化较大,例如 Hook 顺序变化、组件类型变化,React 无法安全保持状态,就会重新初始化组件。
Webpack Loader 和 Plugin 区别?
Webpack 构建流程是什么?
Vite 为什么生产环境还需要打包?
如何优化 Webpack 构建速度?
前端项目如何做 CI/CD?
Docker 部署前端项目流程?