-
Notifications
You must be signed in to change notification settings - Fork 56
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2a60629
commit 41f6417
Showing
1 changed file
with
175 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
# weex 活动落地页性能优化实践 | ||
|
||
考拉的活动落地页已经应用 weex 一段时间了,从 0 到 1 的应用过程中,由于自身特殊的业务场景,我们碰到了许多包括功能、性能方面的问题,本文着重对性能优化方面进行简单总结,主要讲述优化时的分析思路和方案,不涉及过多代码细节。 | ||
|
||
## 滚动渲染 | ||
|
||
考拉的活动落地页最大的特点是商品多、页面长。考拉大促主会场需要展示超过 600 个商品,考拉的大促主会场需要 60 多屏,甚至高达 80 屏。渲染大数据量页面时,我们遇到的难题: | ||
|
||
1. 数据量大,高达几万个节点,一次性全部渲染会导致页面卡顿甚至无响应 | ||
2. 每个模块的结构、样式、数据都不太相同,所以类似无限列表那种重复利用节点的方式进行渲染并不适用 | ||
3. weex sdk 版本仍然是比较老的版本,不能使用 `recycle-list` 组件 | ||
4. 存在电梯导航组件,无法做滚动加载 | ||
|
||
虽然页面总共有几十屏,但同一时刻用户能看到的数据只有一屏高,可以根据用户滚动的位置渲染对应的模块,其他处于不可见的模块就不进行渲染。但页面中就配置电梯导航组件(即点击后跳到对应对的位置),这种特殊的交互行为会带来一些问题:从模块 1 跳到模块 10,然后页面页面往上滚动。由于不可见的模块不进行渲染,页面重新定位后,只有模块 1 和模块 10 两个渲染了,其他模块不可见。当用户下滑,页面往上滚动时,再去渲染模块 9。此时,由于模块 9 的渲染,模块高度会发生变化,引起页面抖动,用户体验很差。解决有两种方案: | ||
|
||
1. 在定位到模块 10 时,将模块 10 以前所有的模块都进行渲染。抖动问题可以解决,但是由于渲染模块数量太多,点击电梯导航后会出现较长时间的延迟,带来了新的用户体验问题 | ||
2. 对于不可见的模块,渲染模块骨架进行占位,在实际模块渲染出来时由于高度不变也不会引发页面抖动,但这要求提供骨架高度准确 | ||
|
||
我们采用的是第二种方案,虽然会让开发变得麻烦,但是目前来说较为合适的方案。社区里面有模块骨架高度计算的方法,在构建时利用 css 样式进行计算。但是活动落地页的场景特殊,同一个模块的展示形式会依赖其数据决定,因此同一种模块数据不一样,其高度也可能不一样。在开发时,每个模块以组件的形式进行开发,并提供一个计算高度的方法 `height(data)`,获取模块数据后,通过这个方法计算出模块的骨架高度。 | ||
|
||
活动落地页滚动渲染的方案可以总结为: | ||
|
||
1. 异步获取模块数据并缓存 | ||
2. 立即渲染第一屏 | ||
3. 非可见区域,先通过模块静态方法计算骨架高度,渲染模块骨架 | ||
4. 滚动中渲染可见区域,并预先渲染 1.5 屏左右的高度 | ||
|
||
## 容错 | ||
|
||
Vue 在编译后会生成一个 `render()` 方法以在运行时生成 virtual dom,如果 `render()` 执行时抛出异常,由于 weex sdk 的特殊机制,会导致页面渲染会终止,未渲染的节点会停止渲染,出现大面积空白,功能无法正常使用。在业务快速迭代的开发中,很难保证在 render 时不出现异常(常见异常是在模版中引用一个不存在变量的属性),所以前期经常出现这种白屏现象。为了解决这一个问题,需要对 weex-vue-loader 进行改造,在生成的 render 方法中通过 `try-catch` 捕获异常,避免影响页面后续的渲染。除了容错,还需要在发生错误时将错误信息进行展示或者上报,以便进行问题定位和处理。所以在异常发生时,会调用挂在 `weex.config` 上的自定义方法,以进行异常处理,可以在方法中返回展示异常信息的 vdom,同时将异常上报。 | ||
|
||
```javascript | ||
render() { | ||
try { | ||
// 生成 vdom | ||
return this._c() | ||
} catch(err) { | ||
return weex.config.nodeErrorHandler(err) | ||
} | ||
} | ||
``` | ||
|
||
## 页面内存优化 | ||
|
||
通常情况是不太需要注意页面内存占用问题,但由于考拉的活动落地页所要渲染的节点过太多,在 iOS 端,尤其是内存只有 1G 的老机型中,会出现由 oom 引发的 app 崩溃现象。一个高度由 60 屏的活动页,在所有模块渲染完成以后,整个 App 的内存占用高达 400MB,纯 weex 的页面内存占用有 300MB,当同时打开多个活动页时,内存占用巨大,系统会强行把 App 杀死。所以,活动页刚刚上线时,iOS 端的 App 崩溃率骤然升高,我们不得不对页面的内存占用进行优化。 | ||
|
||
优化之前,首先需要知道页面中什么占用了内存以及占了多少内存,因为「你无法优化你无法测量的事物」。weex 页面在打开的时候会创建一个 js context,因此可以利用 safari 的 devtool 对 weex 的 js 部分进行分析。另外,可以通过 Xcode 自带的工具对 native 渲染的节点进行分析。 | ||
|
||
### 减少节点 | ||
|
||
weex 官方文档建议控制节点层级,首先就来尝试对模版的部分进行优化。通过 Xcode 的分析工具可以对页面元素节点进行分析,可以发现,有许多宽高一直的节点。例如需要展示一张图片,代码里用 div 包裹 image,但是实际上这两个元素的标签的宽高时完全一样的,即使把 div 去掉,所展示的布局效果不变。这里的 div 就是一个冗余的节点。 | ||
|
||
```html | ||
<div> | ||
<image></image> | ||
</div> | ||
``` | ||
|
||
通过 Xcode 的分析工具可以看到,一个一排二的商品列表模块,一行里面就可能有 4 ~ 5 个冗余节点,如果一个页面里面有 600 个商品,300 行,那这里就有上千个节点,说明这里有优化空间。 | ||
|
||
![节点-前](https://haitao.nos.netease.com/835408b8-5a12-44ab-862b-6d01b4ec270c_2046_1010.jpg) | ||
|
||
经过优化以后,冗余节点被移除,保证布局效果的前提下,节点数和层级都减少了。 | ||
|
||
![节点-后](https://haitao.nos.netease.com/0e922ee7-6369-4be1-800e-20b5a1e437d1_1712_862.jpg) | ||
|
||
模版的优化要求对业务模块代码进行修改,对于上百个业务模块,一个个去优化会有巨大的工作量。在这里,我们可以采用「二八定律」,先对使用量达到 80% 的那几个业务模块进行优化,以求最高的性价比。这里展示的一排二商品模块就是被大量使用的模块之一。 | ||
|
||
经过测试,虽然总的节点数下降了,渲染性能略微有提升,但页面总占用内存数并没有明显变化。这里减少的节点,对于 native 而言,是可见区域内的节点数,由于 weex 的 list 组件会对 cell 中不可见区域的节点进行回收,所以这部分优化对于内存占用而言没有明显效果。而对于 js 部分而言,减少的时 virtual dom 的节点数据,由于单个 vnode 占用的内存并不是太多,所以减少 1000 个 vnode 也不会有质的改变。 | ||
|
||
### 减少 watcher | ||
|
||
通过 xcode 可以看到,全页面渲染完成后,整个 app 内存占用 439MB,页面占用约 300MB。 | ||
|
||
![总内存](https://haitao.nos.netease.com/b93019c3-bea8-42f8-827a-8d91d57bb6ac_528_281.jpg) | ||
|
||
而通过 safari 的 devtool 可以看到 js 部分单单是 Object 对象的内存占用却高达 200MB。可以说,js 部分的内存占用是大头,有很大的优化空间。 | ||
|
||
![js context](https://haitao.nos.netease.com/d8eae61b-d13f-4f35-8382-5b8d0bfc7c35_1262_229.jpg) | ||
|
||
进一步分析可以看到,其中大量的对象都带有 `__ob__` 属性,显然是 Vue 创建的 watcher。虽然每个对象看着不大,但由于数量非常大,所以总体占用会比较多。 | ||
|
||
![watcher-object](https://haitao.nos.netease.com/9d9c69db-9e1e-499e-adb3-1ddf9933ff06_1272_275.jpg) | ||
|
||
活动落地页的大部分模块都是偏展示,即展示的数据很多都不会因为交互而发生即时的变化,因此,这部分数据实际上是不需要创建 watcher 的。Vue 的官方文档上有[说明](https://cn.vuejs.org/v2/guide/instance.html#数据与方法),通过 `Object.freeze` 可以阻止 Vue 为对象的数据创建 watcher。 | ||
|
||
针对这部分模块,在获取了数据以后,通过调用 `Object.freeze` 方法对这些不需要修改的数据进行冻结,阻止的 watcher 的创建(要注意的是,Object.freeze 只能冻结一级属性,需要自行实现 deepFreeze)。在这里同样使用「二八定律」,只修改高频模块。 | ||
|
||
优化后,对象的数据大大降低,从 433k 下降至 354k,保留内存从 203MB 降低至 170 MB。 | ||
|
||
![freeze 对象](https://haitao.nos.netease.com/b7dae9f4-c4f1-48d4-85b9-9fbbd6cdf69f_1266_240.jpg) | ||
|
||
而 App 的内存占用也从 439MB 下降至 385MB,下降 12%,效果明显。 | ||
|
||
![freeze 内存](https://haitao.nos.netease.com/f2436b04-2bca-409a-a79b-f508362221e7_469_225.jpg) | ||
|
||
### 滚动回收 | ||
|
||
但由于页面数量太多,页面中需要创建的组件实例也非常多,要从根源上解决,就要减少页面的数据量。然而我们却无法限制运营所配置的页面数据量,那是不是就没有办法了呢? | ||
|
||
我们再仔细分析一下,weex 的 `list` 组件会回收不可见区域内的 `cell` 中所有节点的内存。但这部分内存包括哪些部分呢?是只回收 native 的视图部分,还是包括了 js context 部分呢?通过 devtool 我们发现,当一个模块处于不可见区域内时,它的对应的组件实例仍然保留,包括它的 vnode 和子组件。weex 所回收的仅仅是 native 的视图部分。 | ||
|
||
```html | ||
<list> | ||
<cell> | ||
<!-- 当 cell 处于不可见区域时,内部元素会被回收 --> | ||
<div></div> | ||
<moduleA></moduleA> | ||
</cell> | ||
</list> | ||
``` | ||
|
||
由于页面中存在大量的业务模块,每个模块又包含多个组件,因此组件实例的数量也非常多,而因为处于可见区域的模块仅有几个,所以绝大部分模块都是处于不可见区域的。 | ||
|
||
![不可见](https://haitao.nos.netease.com/7c8d0f2c-848f-41e1-b7f9-925f8faf8074_310_912.jpg) | ||
|
||
之前,我们为了解决渲染卡顿而滚动中去渲染模块,同时在这滚动的过程中,对于移动到不可见区域的模块,我们可以对其进行回收,将其恢复到骨架占位的状态,将渲染创建的 vm 和 vdom 全都回收掉,减少 vm 和 vnode 的总量。 | ||
|
||
```html | ||
<list> | ||
<cell> | ||
<!-- 通过标识位控制渲染 --> | ||
<moduleA v-if="moduleA.inSight" /> | ||
<skeleton v-else /> | ||
</cell> | ||
</list> | ||
``` | ||
|
||
在应用了滚动回收以后,前页面渲染后,对象的数据从 354K 下降至 87k,App 的内存从 385MB 下降至 279MB,效果明显。 | ||
|
||
![回收-内存](https://haitao.nos.netease.com/ad6ce387-ada1-47be-ae8f-db25554644a9_1265_220.jpg) | ||
|
||
经过之前从小粒度(节点、属性)到大粒度(模块、组件)的优化,单个内存有明显下降明显,App 总的内存占用从 439MB 下降至 385MB,除去 App 自身运行是需要的 110MB,页面的内存占用从 329MB 下降至 169MB,下降幅度达 48%,优化效果明显。 | ||
|
||
![内存 trend](https://haitao.nos.netease.com/37e29489-51ac-42a3-8204-c1c61f3585be_1298_575.jpg) | ||
|
||
在优化时,为了控制影响面积,我们一直都是采用二八原则,无论时冻结属性还是滚动回收,都只对几个高频的业务模块生效,因此对于不同的活动页面配置,由于模块比例不同,其效果也不同。对几个不同类型的页面进行测试,内存下降的比例保持在 20% ~ 50% 之间。 | ||
|
||
### 限制历史栈 | ||
|
||
在优化后,即便单个页面内存占用减少了,但如果对同一时间打开的页面数量不作限制,App 的总内存占用也会不断上升,直至崩溃。因此需要对此进行页面的历史栈大小进行限制。 | ||
|
||
1. App 限制大小,一般可以限制 5~6,但考拉的活动页面实在太大,针对不同的机型,可能需要一个动态的大小,1G 内存的机子限制为 1~2,内存大一点的机子限制为 3~4 | ||
2. App 提供关闭上一个页面的接口,由前端业务代码控制页面关闭逻辑,对于同一类型(主会场活动页)的大型页面,通过调用这个接口关闭上一个页面,控制此类页面的数量 | ||
|
||
限制历史栈大小的方法还有很多,具体需要根据实际的业务场景去定制,而且通常需要 App 端提供相关能力。 | ||
|
||
## 文件拆分 | ||
|
||
weex 本身只提供了打包单个文件 bundle 的能力,无法像在 h5 那样进行按需加载。目前活动页的模块种类已经解决两百个,而每个页面实际所需要的模块类型一般只有 10 个左右,因此没每次发布新版本时,App 都需要下载一个将近 2MB 的 js 文件。为了解决这一个问题,我们需要按照当前页面所需要的模块进行代码拆分。 | ||
|
||
1. 构建时,会按照配置将每个模块输出到不同的 chunk 中,并将不同 chunk 对应的 module 信息放到 manifest.json 中保存 | ||
|
||
``` | ||
common.js | ||
module-a.js | ||
module-b.js | ||
``` | ||
|
||
2. App 在打开一个 weex 活动页面时会先访问一个接口服务,服务根据页面 url 和 manifest.json 计算出当前页面的所需要的文件列表 | ||
3. App 根据文件列表下载对应 js 文件,下载完成后将多个文件拼接成一个 js 文件,然后加载执行 | ||
|
||
将每个页面所需要的 js 文件总大小限制在几百 KB,且不会随着业务模块增多而变得不可控。 | ||
|
||
|
||
## 总结 | ||
|
||
weex 在 native 端的渲染性能很好,但对于长列表而言,其内存占用仍然有优化空间。虽然最新版的 sdk 已推荐使用 recycle-list,但是考拉由于历史原因和业务场景,无法直接使用。在针对大数据的页面渲染时,无论是滚动渲染、滚动回收还是文件拆分,其实其原则都是「按需使用」,不管总体的数据量有多大,同一场景、同一时刻用户所能接触到的数据量是有限的。得益于 weex 的高性能渲染能力,无论时首次渲染还是二次重新渲染,都能保证较好的用户体验。 | ||
|
||
## 参考 | ||
|
||
- [Vue 官方文档](https://cn.vuejs.org/v2/guide/instance.html#数据与方法) | ||
- [Weex 官方文档](https://weex.apache.org/zh/docs/components/list.html#简介) | ||
|
||
by [elcarim5efil](https://github.com/elcarim5efil) |