知识点
- 优化渲染频率
- 优化长列表渲染效率
- 优化长列表图片缓存
不知道各位在调试首页的时候有没有发现首页还是比较卡的,如果这个时候调起 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 中比较重要的优化就完成了,如果还需要再优化就要具体的情况具体分析了。