• React Native性能优化红宝书


    一、React Native介绍

    React Native 是Facebook在React.js Conf2015 推出的开源框架,使用React和应用平台的原生功能来构建 Android 和 iOS 应用。通过 React Native,可以使用 JavaScript 来访问移动平台的 API,使用 React 组件来描述 UI 的外观和行为。

    JS实现调用的能力中间的适配层提供了一些桥接方案

    1、RN优势

    1.1 HTML/CSS/JS开发成本低,用统一的代码规范开发移动端程序,不用关注移动端差异

    1.2 天然跨平台,开发一次,可以生成Android和ios两个系统上的APP,这减少了开发人员需要编写不同版本的应用程序的时间和工作量。

    1.3 无审核热更新

    1.4 可扩展

    1.5 易学易用:React Native 基于 React,开发人员可以使用熟悉的 JavaScript 和 React 组件模型来构建应用程序,因此很容易学习和上手。

    RN的本质是把中间的这个桥Bridge给搭好,让JS和native可以互相调用。

    RN为我们提供了JS的运行环境,所以前端开发者们只需要关心如何编写JS代码,

    画UI只需要画到virtual DOM 中,不需要特别关心具体的平台。

    至于如何把JS代码转成native代码的脏活累活,RN底层全干了

    2、RN劣势

    2.1 不成熟,项目版本更新维护较频繁,学习成本高,试错成本高,有些问题较少解决方案,开发进度慢

    2.2 性能:整体性能仍不如原生

    2.3 兼容性:涉及底层的功能,需要针对Android和ios双端单独开发

    2.4 有限的第三方库:尽管 React Native 社区不断增长,但相对于其他混合应用框架,第三方库和插件的数量还是有限的。这可能使开发人员在某些方面受到限制。

    2.5 有些原生功能需要自己实现:虽然 React Native 提供了大量原生组件,但某些原生功能需要开发人员自己实现,这可能需要额外的时间和工作量。

    3、跨平台框架比较

    开发模式 

    原生开发 

    混合开发 

    Web开发 

    运行环境

    Android、iOS、Windows

    混合App、

    React Native,Weex、Flutter

    浏览器、WebView

    编程语言

    Java,Objective-C

    JavaScript、Dart

    HTML、CSS、JavaScript

    可移植性

    一般

    开发速度

    一般

    性能

    一般

    学习成本

    一般

    4、Hybrid App 优势

    4.1  跨平台开发

    4.2  离线访问

    4.3  原生应用程序的用户体验

    4.4  快速开发和迭代

    混合应用程序结合了Web应用程序和本地应用程序的优点,既能够在原生应用程序环境中运行,也能够在 Web 浏览器中运行,并且具有更好的用户体验和功能。这种开发方式可以大大减少开发时间和成本,提高开发效率。

    5、React Native 0.68之前的架构

    5.1、整体UI渲染太过于依赖JsBridge,容易出现阻塞影响整体UI体验

    5.2、性能和问题反馈最大的组件:

            ScrollView:一次渲染不做任何回收,启动性能慢,占用内存大

            FlatList:做了组件回收,快速滑动中容易出现白屏和卡顿

            Shadow层最终呈现到原生的UI是异步的,滑动太快会有大量的UI

    5.3、事件阻塞在JsBridge,导致了长时间白屏的出现

    6、RN中的视图渲染

    javascript:JS代码的执行线程,将源码通过Metro打包后传给JS引擎进行解析:

            结构 样式 属性

    1. Main线程(UI线程或原生线程):主要负责原生渲染(Native UI)和调用原生模块(Native Modules 
    2.  Shadow 线程Layout线程:创建Shadow Tree来模拟Reac结构树(类似虚拟DOM),再由Yoga引擎将Flexbox等样式,解析成平台的布局方式:

                    宽高  位置

    7、RN架构设计--新旧架构对比

    7.1、JavaScript层: 

    支持React 16+新特性

    增强JS静态类型检查(CodeGen)

    引入JSI允许替换不同的JavaScript引擎

     7.2、Bridge层:

    划分了Fabric和TurboModules,分别负责管理UI和Native模块

     7.3、Native层:

    精简核心模块,将非核心部分拆除去,作为社区模块,独立维护

    8、从 0.68 版本开始React Native 的新架构

    8.1、JSI (JavaScript Interface) 

            一个用 C++ 写成的轻量级框架

            实现JS引擎的互换

            通过JS直接调用Native

            减少不必要的线程通信,省去序列化和反序列化的成本,

            减轻了通信压力,提高了通信性能

    8.2、CodeGen

            FaceBook推出的代码生成工具

            加入了类型约束后,减少了数据类型错误

            自动将 Flow 或 TS 有静态类型检查的 JS 代码转换为 Fabric 和 TurboModules 使用的原生代码

    8.3、优化Bridge层

            Fabric是整个框架中的新UI层,简化了之前的渲染

            Turbo Modules通过JSI可以让JS直接调用Native模块,实现同步操作,实现Native模块按需加载,减少启动时间,提高性能

    8.4、精简核心代码(LEAN Core)

            将React Native核心包进行瘦身,非必要包移到社区单独维护

    9、React介绍

    Facebook,Learn Once,write anywhere

    数据驱动,状态决定界面的变化,虚拟dom(抽象层)

    虚拟dom的好处,首先是性能的优化,不直接操作Dom先进行一些比较,计算找出需要变化的部分,在实际的dom中进行操作

            其次,这个抽象层,还提供了逻辑代码跨平台移植的可能性

    虚拟dom的下方对接的,是网页,是移动端,是电视端,任何一个可以渲染的端,都可以做接口的适配,使得代码在任何地方使用

    主要四个部分:

    组件:类组件、函数式组件

    属性Props

    状态State

    jsx:React 和 React Native 都使用JSX 语法,这种语法使得你可以在 JavaScript 中直接输出本质上也就是 JavaScript,所以你可以在其中直接使用变量元素,JSX 

    hook api: todo补充

    生命周期: todo补充

    二、性能优化点

    1、RN加载的主要时间消耗

    todo:补充

    2、RN性能优化-拆包、分包

    React Native 页面的 JavaScript 代码包是热更新平台根据版本号进行下发的,每次有业务改动,我们都需要通过网络请求更新代码包。

    因此,我们在对JavaScript 代码进行打包的时候,需要将包拆分成两个部分:

    2.1、Common 部分,也就是 React Native 源码部分;

    2.2、业务代码部分,也就是我们需要动态下载的部分

    具体操作:

    • JavaScript 代码包中 React Native 源码相关的部分是不会发生变化的,所以我们不需要在每次业务包更新的时候都进行下发,在工程中内置一份就好了。
    • Common 包内置到工程中
    • 业务代码包进行动态下载
    • 利用 JSContext 环境,在进入载体页后在环境中先加载 Common 包,再加载业务代码包就可以完整的渲染出 React Native 页面

    todo:代码示例?

    3、首屏渲染

    通过拆包的方案,已经减少了动态下载的业务代码包的大小。但是还会存在部分业务非常庞大,拆包后业务代码包的大小依然很大的情况,依然会导致下载速度较慢,并且还会受网络情况的影响。

    因此,我们可以再次针对业务代码包进行拆分

    将一个业务代码包拆分为一个主包和多个子包的方式

    在进入页面后优先请求主包的 JavaScript 代码资源,能够快速地渲染首屏页面,

    紧接着用户点击某一个模块时,再继续下载对应模块的代码包并进行渲染,就能再进一步减少加载时间。

    4、减少渲染的节点-通过组件懒加载提高应用性能

    组件懒加载可以让 react 应用在真正需要展示这个组件的时候再去展示,有效的减少渲染的节点数,提高页面的加载速度

    React 官方在 16.6 版本后引入了新的特性:React.lazy 和 React.Suspense,这两个组件的配合使用可以比较方便进行组件懒加载的实现

    React.lazy 该方法主要的作用就是可以定义一个动态加载的组件,这可以直接缩减打包后 bundle 的体积,

    并且可以延迟加载在初次渲染时不需要渲染的组件

     Suspense组件中的 fallback 属性接受任何在组件加载过程中你想展示的 React 元素。

    你可以将 Suspense 组件置于懒加载组件之上的任何位置,你甚至可以用一个 Suspense 组件包裹多个懒加载组件。 代码示例如下:

    1. import React, {Suspense} from 'react';
    2. const OtherComponent = React.lazy(() => import('./OtherComponent));
    3. const AnotherComponent = React.lazy(() => import('./AnotherComponent));
    4. function MyComponent() {
    5. return(
    6. <div>
    7. <Suspense fallback={<div> loading...div>}>
    8. <section>
    9. <OtherComponent/>
    10. <AnotherComponent/>
    11. section>
    12. Suspense>
    13. div>
    14. )
    15. }

    使用懒加载可以减少bundle文件大小,

    不同的文件打包到不同的页面组件当中,加快组件的呈现

    1. import React, {lazy, Suspense} from 'react';
    2. const OtherComponentLazy = lazy(() => import('./OtherComponent));
    3. const AnotherComponentLazy = lazy(() => import('./AnotherComponent));
    4. function Suspensed(lazyComponent) {
    5. return (props) {
    6. <Suspense fallback={<div>div>}>
    7. <lazyComponent {...props} />
    8. Suspense>
    9. }
    10. }
    11. export const OtherComponent = Suspensed(OtherComponentLazy)
    12. export const AnotherComponent = Suspensed(AnotherComponentLazy)

    5、提交阶段优化-避免重复无限渲染

    当应用程序状态发生更改时,React会调用render方法。

    如果在render方法中继续更改应用程序状态,就会发生render方法递归调用,导致应用报错。

    Render方法应该作为纯函数,render方法的执行要根据状态的改变,保持组件的行为和渲染方法一致。

    执行提交阶段钩子,会阻塞浏览器更新页面。

    如果在提交阶段钩子函数中更新组件 State,会再次触发组件的更新流程,造成两倍耗时。

    一般在提交阶段的钩子中更新组件状态的场景有:

    • 类组件应使用 getDerivedStateFromProps 钩子方法代替,函数组件应使用函数调用时执行 setState的方式代替。
    • 使用上面两种方式后,React 会将新状态和派生状态在一次更新内完成。
    • 根据 DOM 信息,修改组件状态。在该场景中,除非想办法不依赖 DOM 信息,否则两次更新过程是少不了的,就只能用其他优化技巧了。

    6、组件卸载前执行清理操作

    React 组件性能优化的核心是减少渲染真实DOM节点的频率,减少Virtual DOM 比对的频率

    在组件中为window注册的全局事件,以及定时器,在组件卸载前要清理掉,防止组件卸载后继续执行影响应用性能

    1. import React,{
    2. Component
    3. } from 'react';
    4. export default class Hello extends Component {
    5. componentDidMount() {
    6. this.timer = setTimeout(
    7. () => { console.log('把一个定时器的引用挂在this上'); },
    8. 500
    9. );
    10. }
    11. componentWillUnmount() {
    12. // 如果存在this.timer,则使用clearTimeout清空。
    13. // 如果你使用多个timer,那么用多个变量,或者用个数组来保存引用,然后逐个clear
    14. this.timer && clearTimeout(this.timer);
    15. }
    16. };

    7、通过使用占位符标记提升React组件的渲染性能

    使用Fragment不对应具体的视图,减少了包含的额外标记的数量

    仅仅是代表可以包装而已,跟空的标识符一样,视图层级关系减少有利于视图渲染,

    满足在组件顶级具有单个父级的条件

    1. <div>
    2. ...
    3. div
    4. // 上面会多出一个无意义标记
    5. // 应该改为 
    6. <fragment>
    7. ...
    8. fragment>
    9. // 或者写成下面这样也是可以的
    10. <>
    11. ...

    8、缩小状态影响范围-状态下放到使用组件的内部

    React不直接操作DOM,它在内存中维护一个快速响应的DOM描述,render方法返回一个DOM的描述

    React能够计算出两个DOM描述的差异,然后更新浏览器中的DOM,这就是著名的DOM Diff。

    就是说React在接收到属性(props)或者状态(state)更新时,就会通过前面的方式更新UI。所以React整个UI渲染是比较快的。但是这里面可能出现的问题是:

    假设我们定义一个父组件,其包含了5000个子组件。我们有一个输入框输入操作,每次输入一个数字,对应的那个子组件背景色变红。

    父组件更新默认触发所有子组件更新,子组件的props为空对象,{} === {} 永远会返回false

    优化思想:将变的和不变的分开(state,props,context)

    我们需要先手动创建一个有严重渲染性能的组件,如下所示:

    1. import { useState } from 'react';
    2. export default function App() {
    3. let [color, setColor] = useState('red');
    4. return (
    5. <div>
    6. <input value={color} onChange={(e) => setColor(e.target.value)} />
    7. <p style={{ color }}>Hello, world!p>
    8. <ExpensiveTree />
    9. div>
    10. );
    11. }
    12. function ExpensiveTree() {
    13. let now = performance.now();
    14. while (performance.now() - now < 100) {
    15. // Artificial delay -- do nothing for 100ms
    16. }
    17. return <p>I am a very slow component tree.p>;
    18. }

    很显然,当 App 组件内的状态发生了改变,ExpensiveTree 组件会 re-render, 事实上 ExpensiveTree 组件的 props、state 并未发生改变,这并不是我们期望的结果。

    1. export default function App() {
    2. let [color, setColor] = useState('red');
    3. return (
    4. <div>
    5. <input value={color} onChange={(e) => setColor(e.target.value)} />
    6. <p style={{ color }}>Hello, world!p>
    7. <ExpensiveTree />
    8. div>
    9. );
    10. }

    我们可以看到以上 ExpensiveTree 组件并不依赖 App 组件内部的状态,因此我们是否可以考虑,将依赖 color 的元素抽离到一个依赖 color 的组件中呢?

    1. export default function App() {
    2. return (
    3. <>
    4. <Form />
    5. <ExpensiveTree />
    6. );
    7. }
    8. function Form() {
    9. let [color, setColor] = useState('red');
    10. return (
    11. <>
    12. <input value={color} onChange={(e) => setColor(e.target.value)} />
    13. <p style={{ color }}>Hello, world!p>
    14. );
    15. }

    此时,将依赖 color 的元素提取到了 Form 组件中,Form 组件内部的状态不再影响 ExpensiveTree 组件的渲染,问题便得到了解决。

    9、跳过不必要的组件更新-通过纯组件提升性能

    • 类组件中使用PureComponent
    • 函数式组件中使用React.memo

    在 React 工作流中,如果只有父组件发生状态更新,即使父组件传给子组件的所有 Props 都没有修改,也会引起子组件的 Render 过程。

    如果子组件的 Props 和 State 都没有改变,那么其生成的 DOM 结构和副作用也不应该发生改变。

    纯组件会对组件输入数据进行浅层比较,如果当前输入数据和上次输入数据相同,组件不会重新渲染。

    比较引用数据类型在内存中的引用地址是否相同,比较基本数据类型的值是否相同

    为什么不直接进行diff操作?而要先进行浅比较?

    浅比较只操作当前组件的state和props,diff操作会重新遍历整个VirtualDOM树

    10、跳过不必要的组件更新-通过shouldComponentUpdate

    纯函数只能进行浅层比较,要进行深层比较,

    使用shouldComponentUpdate,它用于编写自定义比较逻辑

    1. ShouldComponentUpdate(nextProps,nextState){
    2.         if(nextProps……){
    3.           return true
    4.         }
    5.         return false
    6. }

    11、跳过不必要的组件更新-通过memo纯组件提升性能

    Memo基本使用:将函数组件变为纯组件,当前props和上一次的props进行浅比较,如果相同就阻止组件重新渲染

    Memo函数是浅层数据比较,如果遇到引用数据类型,需要传递自定义比较逻辑,传给Memo函数的第二个参数

    memo的第二个参数示例如下:

    1. // 给memo传递第二个参数,自定义比较逻辑
    2. import React, { memo, useEffect, useState } from 'react'
    3. function App () {
    4.   const [person, setPerson] = useState({ name'张三'age20job'waiter' })
    5.   const [index, setIndex] = useState(0)
    6.   useEffect(() => {
    7.     let timer = setInterval(() => {
    8.       setIndex(prev => prev + 1)
    9.       setPerson({ ...person, job'chef' })
    10.     }, 1000)
    11.     return () => {
    12.       clearInterval(timer)
    13.     }
    14.   }, [index, person])
    15.   return (
    16.     <div>
    17.       {index}
    18.       <ShowName person={person} />
    19.     div>
    20.   )
    21. }
    22. function compare (prevProps, nextProps) {
    23.   if (
    24.     prevProps.person.name !== nextProps.person.name ||
    25.     prevProps.person.age !== nextProps.person.age
    26.   ) {
    27.     return false
    28.   }
    29.   return true
    30. }
    31. const ShowName = memo(function ({ person }) {
    32.   console.log('render...')
    33.   return (
    34.     <div>
    35.       {person.name} {person.age}
    36.     div>
    37.   )
    38. }, compare)
    39. export default App

    12、跳过不必要的组件更新- useMemo、useCallback缓存优化

    useMemo、useCallback 实现稳定的 Props 值

    useMemo 缓存上次计算的结果,当 useMemo 的依赖未发生改变时,就不会触发重新计算,一般用在非常耗时的场景中,

    如:遍历大列表做统计信息。

    useCallback ,将父组件传递给子组件时,子组件会因为重新render发生改变的时候,缓存一个值

    useCallback 是 React 的一个 Hook,它用于记住函数的引用,避免在每次渲染时都创建一个新的函数实例。这可以帮助优化性能,因为避免不必要的重渲染和子组件的重新渲染。

    使用场景:

    当你有一个函数,它依赖于某些 props 或 state,但你不希望它在这些依赖发生变化时重新创建,你可以使用 useCallback 来保持引用不变。

    如果你有一个子组件,它是纯的(不依赖外部状态,只依赖于传入的 props),并且你希望避免非必要的重渲染,你可以使用 useCallback 来保证传递给子组件的函数引用保持不变。

    例子代码:

    1. import React, { useCallback } from 'react';
    2. function ParentComponent() {
    3. const [count, setCount] = useState(0);
    4. // 使用 useCallback 来避免在每次渲染时都创建一个新的函数实例
    5. const incrementCount = useCallback(() => {
    6.   setCount(count + 1);
    7. }, [count]);
    8. return (
    9.  <div>
    10.       <p>Count: {count}

    11.       <button onClick={incrementCount}>Incrementbutton>
    12.         ChildComponent onIncrement={incrementCount} />
    13.  div>
    14. );
    15. }
    16. function ChildComponent({ onIncrement }) {
    17. // 这里我们不需要每次 ParentComponent 渲染时都创建一个新的函数实例
    18. // 因为 onIncrement 引用没有变化
    19.         return <button onClick={onIncrement}>Increment from Childbutton>;
    20. }

    在这个例子中,incrementCount 是一个函数,用于增加计数。我们使用 useCallback 来保证在 ParentComponent 的多次渲染中,incrementCount 函数的引用是不变的,这样 ChildComponent 就不会因为 ParentComponent 的渲染而不必要地重新渲染。

    13、列表使用Key属性

    遍历展示视图时使用 key,key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识

    1. const numbers = [1, 2, 3, 4, 5];
    2. const listItems = numbers.map((number) =>
    3. <li key={number.toString()}> {number} li>);

    使用 key 注意事项:

    这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的 id 来作为元素的 key

    14、不要使用内联函数定义:

    如果使用内联函数,函数是引用数据类型,在内存中的地址不会相同,则每次render函数时,都会创建一个新的函数实例

    在渲染阶段会绑定新函数并将旧实例扔给垃圾回收,因此绑定内联函数,需要额外的垃圾回收和绑定到DOM的工作

    1. 1.Render(){
    2.         Return (<input type=“button” onClick={(e)=>{this.setState({inputVal:e.target.value})}}>)
    3. }
    4. 2.setNewState=(e)->{this.setState({inputVal:e.target.value})}
    5. //箭头函数被添加到类的实例对象属性而不是原型对象属性,如果组件被多次重用,每个实例都会有一个相同的函数实例,降低了函数实例的可重用性,造成了资源的浪费
    6. Render(){
    7.         Return (<input type=“button” onClick={(e)=>{this.setNewState}/> )
    8. }
    9. 3.Export default class a extends React.Component{
    10.     constructor(){
    11.         this.handleClick = this.handClick.bind(this)//构造函数只执行一次
    12.     }
    13. }
    14. handleClick(){xxxxx}
    15. <input type=“button” onClick={this.handleClick}/>
    16. //render方法每次执行时都会调用bind方法生成新的函数实例
    17. <input type=“button” onClick={this.handleClick.bind(this)}/>

    15、避免使用内联样式

    使用内联样式时,会被编译为JavaScript代码,JavaScript将样式规则映射到元素上,浏览器需要花费更多时间处理脚本和渲染UI。

    它是在执行时为元素添加样式,而不是在编译时为元素添加样式。

    因此,性能非常的低,更好的办法是将CSS文件导入组件。能通过css直接做的事情,就不要通过JavaScript去做,因为JavaScript操作DOM非常慢

    16、事件节流和防抖

    利用debounce、throttle 避免重复回调

    节流:

    意味着不会立即执行,在触发事件之前会加上几毫秒延迟,

    throttle 更适合需要实时响应用户的场景中更适合,

    如通过拖拽调整尺寸或通过拖拽进行放大缩小(如:window 的 resize 事件)。实时响应用户操作场景中,如果回调耗时小,

    甚至可以用 requestAnimationFrame 代替 throttle。

    页面滚动到底部的时候,使用节流,否则触发多个网络请求调用,导致性能问题

    防抖:

    在输入框中键入数据,直到用户不再输入数据为止,兼容网络调用提升性能

    在搜索场景中,只需响应用户最后一次输入,无需响应用户的中间输入值,

    debounce 更适合使用在该场景中。

  • 相关阅读:
    项目中的奇葩需求你都怎么应对?
    前段入门-CSS
    python+java+nodejs+Vue便捷式管理系统(酒店,车票,旅游攻略)的设计php
    【原创】基于JavaWeb的社区疫情防控管理系统(疫情防控管理系统毕业设计)
    现有的 NFT 协议
    端到端流程总结
    10分钟了解JWT令牌 (JSON Web)
    人工智能对就业的影响:机遇与挑战
    【C++】函数重载 ② ( 重载函数调用分析 | 函数重载特点 | 函数重载与默认参数 )
    Vue2项目练手——通用后台管理项目第二节
  • 原文地址:https://blog.csdn.net/franktaoge/article/details/139836134