【React Native 实战:构建电商 App】15-优化 App,提升10倍效率

React Native 实战:构建电商 App

知识点

  • 优化渲染频率
  • 优化长列表渲染效率
  • 优化长列表图片缓存

不知道各位在调试首页的时候有没有发现首页还是比较卡的,如果这个时候调起 RN 的调试窗体,就会发现在滑动的过程中 js 线程的帧率会很低。如果首页再加上一个 tab 组件的话,这个渲染的帧率会下降的更加厉害。

这里就教大家怎么优化首页这里的性能,其他页比较简单,原理也都是一样,就不再赘述了。

优化刷新次数

这里有一个非常主要的优化逻辑,已经创建好的组件就不需要再次渲染,除非有 UI 层的变化。

这里在 GoodItem 组件下添加一个输入,看看到底有多少次渲染。

componentWillUpdate(){
  console.log("=======刷新=======")
}

然后 App,再上划看看,在浏览器的 Console 中发现了非常多的输出信息,按照之前的逻辑,一个 gooditem 其实只应该渲染一次,这里发现这么多的渲染,显然是不合逻辑的。

这是利用组件内置的 shouldComponentUpdate 方法来阻止无用的刷新,下面提供一个非常简单的方法阻止刷新。在组件内部创建一个状态变量 shouldUpdate,然后判断如果这个变量是真则刷新一次。

//状态变量
shouldUpdate = true;
//判断是否需要刷新
shouldComponentUpdate() {
    if (!this.shouldUpdate) return false;
    return !(this.shouldUpdate = false);
}

再次刷新 App,上划看看效果,这次的结果和上次相比较就有了非常大的改观,再长列表的时候因为这种情况会很容易发生 App 闪退的现象。如果使用这种方法,JS 和 Native 之间的无用通讯就减少到非常少的地步了,性能提升何止 10 倍。

如果想再次刷新界面呢?只需要在有改动的时候,比如 setState 之前调用一次 this.shouldUpdate = true; 就可以主动刷新界面了。

优化长列表

现在再次浏览首页,发现首页还是存在卡顿的现象,如果手机的性能是厂家优化过的,还会发现在某些时候长列表很慢,或者一片空白。

这个时候就要改造列表的结构了,由于之前自定义模板是一个循环之前输出的,造成了 FlatList 不能很好的识别模块的距离,在主动隐藏列表超出视界的节点时不能很好的作用在目前的节点上。

我们将所有节点放入列表的数据中,根据列表的类型来判断是使用模板还是产品。

我们将首页分成 banner、模板、产品三个部分,根据不同的方法加载数据,并设置为不同的类型。

//加载banner数据,设置类型为banner
async loadBanner() {
    try {
        let res = await request.get(`/banner/findBannerAndQuickList.do?categoryId=${this.props.id}`);
        res.type = "banner";
        res.index = "banner";
        res.tt = Date.now();
        let h = 0;
        //判断是否存在banner
        if (res.bannerList.length > 0) {
            h =  px(480);
        }
        //判断是否存在快捷按钮
        if (res.quickList.length > 0) {
            h += px(210)
        }
        //添加高度信息
        this.layout[0] = { h };
        this.setState({ list: [res] });
    } catch (e) {
        // toast(e.message);
    } finally {
        this.step = 1;
        this.loadNext();
    }
}

这个地方使用 step 来保证只加载一次 banner 和模板,剩下的一直加载产品列表。

//加载下一页,这里判断到底应该加载哪一块
async loadNext() {
     if (this.step === 1) this.getModules();
     if (this.step === 2) this.getNextProducts();
 }

之前的楼层是一次性渲染整个列表,这里分开渲染,将列表拆分成一行一行的组件,并且在渲染的时候阻止多次无效渲染。

//获取楼层
async getModules() {
    if (this.loading) return;
    this.loading = true;
    let list = [];
    try {
        let moduleList = await request.get(`/module/findModuleListV2.do?categoryId=`);
        if (moduleList.constructor === Array) {
            moduleList.forEach((item, key) => {
                item.type = 'module';
                item.index = 'module_' + item.moduleId;
                item.key = key;
                list.push(item);
                this.layout.push({ h: 0 });
            })
        }
    } catch (e) {
        // toast(e.message);
    } finally {
        this.step = 2;
        this.loading = false;
        const list1 = this.state.list[0];
        //这里加入一个单独的产品头部图片
        const tit = { type: "title", index: "title" };
        list.push(tit);
        this.layout.push({ h: px(100) });
        this.setState({ list: this.state.list.concat(list) });
    }
}

根据之前的逻辑将产品的加载改造成一个方法,在加载下一页的时候顺便判断是否到页尾了。

//加载商品
async getNextProducts() {
    if (this.start >= this.totalPages) return;
    if (this.loading) return;
    this.loading = true;
    try {
        let res = await request.get(`/goods/list.do?limit=20&start=${this.start}&categoryId=`);
        this.totalPages = res.totalPages;
        let list = [];
        for (let index = 0, j = res.items.length; index < j; index++) {
            const item = res.items[index];
            if (!item) continue;
            let temp = {
                type: "product",
                index: "product_" + item.sku + this.start,
                key: index,
                show:true
            };
            temp.data = item;
            let h = this.productSH;
            if ((index + 1) % 5 !== 0) {
                temp.data2 = Object.assign({}, res.items[index + 1]);
                h = this.productBH;
                res.items[index + 1] = null;
            }
            this.layout.push({ h });
            list.push(temp);
        }
        this.setState({ list: this.state.list.concat(list) });
        if (res.items.length < 20) this.totalPages = 1;
    } catch (e) {
        // toast(e.message);
    } finally {
        this.start++;
        this.loading = false;
        if (this.start >= this.totalPages) {
            this.setState({ loadText: "别扯啦,到底了..." })
        }
    }
}

记得把刷新方法也改一下,不然就会很尴尬的返现后面的东西不加载了。

//刷新
refresh() {
    this.step = 0;
    this.start = 0;
    this.loading = false;
    this.totalPages = 2;
    this.loadBanner();
}

这里提供一下计算模板高度的方法,目前还没有用到这个高度,有兴趣的可以在这里猜一下用这个高度要干嘛。

//计算模板的高度
onLayout(e, index) {
    this.layout[index].h = e.layout.height
}

再次刷新 App,试一下 App 的滑动过程,这次已经可以明显感觉到滑动非常的流畅了。在 iOS 上基本就没有什么问题了,安卓的话因为机型实在太多了,还需要接下来继续优化才行。

优化图片渲染

在经过上述的两种方式优化之后,在安卓手机上还是会出现卡顿甚至闪退的现象,经过我的仔细调试,我发现是渲染图片的问题。在渲染长列表的时候,由于里面涉及到的图片太多了,在不断的上划过程中不断加载图片导致 App 在安卓的低配手机上出现闪退的现象。

优化的方案也非常的简单,就是在界面超出屏幕之后将图片移除即可,这里使用占位图来代替旧的图片。

之前的优化已经做了一部分工作了,比如获取每一个块的高度,这样就可以计算出哪一块还在屏幕上了,
修改 getNextProducts 方法中,将产品的默认显示属性变为 false。

let temp = {
    type: "product",
      index: "product_" + item.sku + this.start,
      key: index,
      show:false
  };

这里为了方便,只做了产品的图片占位替换,真实的 App 中其实也足够了,可以在下图中看到本来的图片已经被一个占位图霸占了,native 渲染的时候其实只需要转码一张图片。

在 FlaList 的滚动事件中添加对当前视图的查询,从这里即可根据之前获取的高度判断到视图和列表块的关系,可以打开注释看一下输出,根据当前的位置会输出列表的位置。

//滚动监听
_onScroll(e) {
    const y = e.contentOffset.y;
    let index = 0;
    let curr = 0;
    while (y > curr) {
        if (!this.layout[index]) break;
        curr += this.layout[index].h;
        index++;
    }
    // console.log("当前第" + index + "行");
    this.showImage(index);

    if (y < 200) this.state.scrollTop.setValue(y)
    if (y < 500 && this.state.showTop) {
        this.setState({ showTop: false })
    }
    if (y > 500 && !this.state.showTop) {
        this.setState({ showTop: true })
    }
}

根据传入的位置来显示隐藏列表中的图片,这里使用一个定时器,避免滚动过程中频繁的改变状态,这里我把 shouldComponentUpdate 注释掉了,这会导致页面刷新频率增多,需要优化的可以参考上面的第一部分优化一下。

//定时器
timer = null;
showImage(index) {
    if (this.timer) return;
    this.timer = setTimeout(() => {
        // console.log("延迟显示:当前第" + index + "行");
        let list = this.state.list.filter((item, i) => {
            item.show = i >= index - 2 && i < index + 5;
            return item;
        })
        // this.shouldComponentUpdate = true;
        this.setState({ list })
        if (this.timer) clearTimeout(this.timer);
        this.timer = null;
    }, 200);
}

到这里 App 中比较重要的优化就完成了,如果还需要再优化就要具体的情况具体分析了。

发表评论