ReactNative性能优化

1,956 阅读7分钟

减少无效render

  1. 利用 shouldComponentUpdate 生命周期进行处理

如下面的 demo 中,当新的 props.event.id 与当前的 props.event.id 相等时,不重新 render

class Base extends Component {
    shouldComponentUpdate(nextProps) {
        return this.props.event.id !== nextProps.event.id;
    }
    // other code
}
  1. 使用 PureComponent

PureComponent 会在 shouldComponentUpdate 对数据进行浅比较,当数据相同时,返回 false

  1. 避免将所有的 props 都传递给子组件,尽量只传递子组件需要的 props

如在使用 PureComponent 进行浅比较时,组件自身所需要的 props 没有发生变化,却因为其它无用 props 导致重新渲染。

无状态组件使用函数式组件替代

React 会面向这种无状态的组件进行一些专门的优化,比如避免无意义的检查或内存分配。所以建议大家尽可能在项目中使用无状态组件,而如果使用无状态组件,建议使用函数式的方式去定义。

function Base(props) {
    return (<h1>Hello, {props.name}</h1>)
}

采用 inlineRequire 的方案,实现按需加载

预先定义模块对象,赋值为 null,在使用时候判断对象是否为 null,如果为 null,则做真正的 require,进行模块加载。

export default class Base extends Component {
    someEvent = () => {
        JDEvent = require('@jdreact/jdreact-core-lib/Libraries/JDEvent')
    }
}

数组动态创建的子组件的情况,为每个子组件添加唯一的 key 属性值

Keys 可以在某些元素被增加或删除的时候帮助 React 识别哪些元素发生了变化。

因此应当给数组中的每一个元素赋予一个确定的标识,这影响 Reactdiff 算法的复杂度。

图片优化

  1. 对图片的大小压缩,服务端下发适合控件大小的图片,避免下发过大的图片

  2. IOS 中适时调用清除图片缓存的 API (列表滑动, 页面返回, tab切换等)

避免一次性过多渲染数据

FlatList 设置 initialNumToRender, 最好刚刚够填满一个屏幕,这样保证了用最短的时间给用户呈现可见的内容。

属性传递优化

  1. 函数传递优化

避免在组件的 props 中使用 bind(this) 或者使用匿名函数,这会导致每次 render,都会创建一个新函数,这样即使数据本身没有发生变化,组件的 props 被改变,导致重新渲染。

不好的写法

class Base extends Component {
    handlePress(e) {
        //code
    }
    render() {
        return (
            <View>
                <JDButton onPress={() => {alert('按钮')}}>按钮1</JDButton>
                <JDButton onPress={this.handlePress.bind(this)}>按钮2</JDButton>
            </View>
        )
    }
}

推荐的写法(在contsrutor中对进行统一绑定,或使用箭头函数)

class Base extends Component {
    constructor() {
        super();
        this.showMore = this.showMore.bind(this);
    }
    showMore(e) {
        //code
    }
    handlePress = (e) => {
        //code
    }
    render() {
        return (
            <View>
                <JDButton onPress={this.handlePress}>按钮</JDButton>
                <JDButton onPress={this.showMore}>按钮</JDButton>
            </View>
        )
    }
}

列表性能优化

VirtualizedList

RN 列表并没有直接使用 IOS 或 Android 的原生列表组件,这是因为 RN 调用原生代码的过程是异步的,而原生列表的渲染要求必须是同步渲染。

早期 RN 版本中,列表组件为 ListView,性能较差。后期版本中,提供了高性能列表组件 FlatListSectionList,二者都是基于 VirtualizedList 实现的。

一般来说,FlatListSectionList 已经能够满足常见的开发需求,仅当想获得比 FlatList 更高的灵活性(比如说在使用 immutable data 而不是普通数组)的时候,才会应该考虑使用VirtualizedList

VirtualizedList 通过维护一个有限的渲染窗口(其中包含可见的元素),并将渲染窗口之外的元素全部用合适的定长空白空间代替的方式,极大的改善了内存消耗以及在有大量数据情况下的使用性能(类似于 AndroidListView 的界面复用机制)。

当一个元素离可视区太远时,它的渲染的优先级较低,否则就获得一个较高的优先级,VirtualizedList 通过这种机制来提高列表的渲染性能。

在使用 VirtualizedList 时注意以下几点

  1. 当某行滑出渲染区域之外后,其内部状态将不会保留,请确保在行组件以外的地方保留了数据。
  2. 本组件继承自 PureComponent 而非通常的 Component,这意味着如果其 props 在浅比较中是相等的,则不会重新渲染。所以请先检查你的 renderItem 函数所依赖的 props 数据。如果是一个引用类型,则需要先修改其引用地址(比如先复制到一个新的 Object 或者数组中),然后再修改其值,否则界面很可能不会刷新。
  3. 列表组件继承自 PureComponent ,因此可以使用 extraData 属性值标记列表需要重新渲染
  4. 为了优化内存占用,同时保持滑动的流畅,列表内容会在屏幕外异步绘制。这意味着如果用户滑动的速度超过渲染的速度,则会先看到空白的内容。
  5. 默认情况下每行都需要提供一个不重复的 key 属性,开发者可以提供一个 keyExtractor 函数来生成 key

源码分析

VirtualizedList 执行的流程如下

  1. 每次新增绘制 item 的最大数量为 10,循环绘制(默认以 10 为单位累加绘制)
  2. 首先绘制显示在屏幕中的 items,再根据优先级循环绘制屏幕上显示 items 相近的数据,直至绘制完成
  3. 每次绘制过程中,所有不需要绘制的元素用空 View 代替

其中,循环绘制的加载方法为 _scheduleCellsToRenderUpdate()

componentDidUpdate() {
    this._scheduleCellsToRenderUpdate();
}

在每次刷新完成后会调用 _scheduleCellsToRenderUpdate 方法,该方法最终会调用 _updateCellsToRender方法

_updateCellsToRender = () => {
    const {data, getItemCount, onEndReachedThreshold} = this.props;
    const isVirtualizationDisabled = this._isVirtualizationDisabled();
    this._updateViewableItems(data);
    if (!data) {
        return;
    }
    this.setState(state => {
        let newState;
        if (!isVirtualizationDisabled) {
            // If we run this with bogus data, we'll force-render window {first: 0, last: 0},
            // and wipe out the initialNumToRender rendered elements.    
            // So let's wait until the scroll view metrics have been set up. And until then,
            // we will trust the initialNumToRender suggestion
            if (this._scrollMetrics.visibleLength) {
                // If we have a non-zero initialScrollIndex and run this before we've scrolled,
                // we'll wipe out the initialNumToRender rendered elements starting at initialScrollIndex.
                // So let's wait until we've scrolled the view to the right place. And until then,
                // we will trust the initialScrollIndex suggestion.
                if (!this.props.initialScrollIndex || this._scrollMetrics.offset) {
                    newState = computeWindowedRenderLimits(
                        this.props,
                        state,
                        this._getFrameMetricsApprox,
                        this._scrollMetrics,
                    );
                }
            }
        } else {
            const {contentLength, offset, visibleLength} = this._scrollMetrics;
            const distanceFromEnd = contentLength - visibleLength - offset;
            const renderAhead =
                distanceFromEnd < onEndReachedThreshold * visibleLength
                    ? this.props.maxToRenderPerBatch
                    : 0;
            newState = {
                first: 0,
                last: Math.min(state.last + renderAhead, getItemCount(data) - 1),
            };
        }
        return newState;
    });
};

_updateCellsToRender 中会调用 setState 方法更新状态,因此在每次绘制完成(状态更新完成)后,都会接着调用更新方法。所以形成了循环绘制的效果。

理论上这种结构会造成无限循环,但是 VirtualizedList 是继承自 PureComponent,所以当检测到状态未改变的时候就会终止更新。

在上述 _updateCellsToRender 方法中,调用了 computeWindowedRenderLimits 生成最新的firstlast,该方法属于 VirtualizeUtils 类。它是根据优先级动态计算 firstlast,距离屏幕显示组件的数据越近,优先级越高。

disableVirtualization

Usage

disableVirtualization

Deprecated. Virtualization provides significant performance and memory optimizations, but fully unmounts react instances that are outside of the render window. You should only need to disable this for debugging purposes.—— RN-VirtualizedList

disableVirtualization 属性已被废弃,该属性提供了显著的性能和内存优化,但是会完全卸载位于可视区之外的 react 实例。当且进当为了调试,你才可以关闭这个特性。

关闭列表复用机制

现象描述

在低端安卓机器和内存小的IOS手机上,使用 FlatList 或者 SectionList,当页面滑动较快时,会出现页面空白,掉帧,图片加载中等情况

现象分析

相比原生,RN列表的复用机制还不太完善。FlatList 虽然有 item 复用机制,但目前不太完善。在打开复用机制时,快速滑动会出现白屏。

问题解决

  • 列表配置优化 | RN官网
  • 关闭列表的复用机制(disableVirtualization = true),然后在页面滑动时手动回收图片
  • 设置合理的 windowSize 属性值,设置可视区外最大能被渲染的元素的数量(以可视区的长度为单位),减小内存消耗并提高性能。
  • 设置 removeClippedSubviews 属性值为 true,在列表视图之外的视图(该视图的 overflow 属性值必须为 hidden) 会从被暂时移除,可以提高滚动性能
  • 设置 maxToRenderPerBatch 属性值,设定每批次(per batch)渲染中最大的渲染列表元素个数
  • 设置 updateCellsBatchingPeriod 属性值,设定批次渲染的频率,即批次渲染的周期
  • 设置 initialNumToRender 属性值,设定初次渲染的列表元素个数
  • 对于列表元素固定高度的情况,使用 getItemLayout 预先指定元素高度
  • 使用 shouldComponentUpdate 函数减少不必要的组件更新
  • 为列表元素添加唯一 keykeyExtractor
  • renderItem 函数中避免使用匿名函数(否则,在每次调用 render 函数时候会重复创建匿名函数)
<FlatList
    //...
    initialNumToRender={9}
    windowSize={10}
    maxToRenderPerBatch={2}
    removeClippedSubviews={true}
    disableVirtualization={true}
    getItemLayout={this.getItemLayout}
    keyExtractor={(item, index) => item[0]}
    extraData={this.state}
/>