Taro:高性能小程序的最佳实践
前言
作为一个开放式的跨端跨框架解决方案,Taro 在大量的小程序和 H5 应用中得到了广泛应用。我们经常收到开发者的反馈,例如“渲染速度较慢”、“滑动不够流畅”、“性能与原生应用相比有差距” 等。这表明性能问题一直是困扰开发者的一个重要问题。
熟悉 Taro 的开发者应该知道,相比于 Taro 1/2,Taro 3 是一个更加注重运行时而轻量化编译时的框架。它的优势在于提供了更高效的代码编写方式,并拥有更丰富的生态系统。然而,这也意味着在性能方面可能会有一些损耗。
但是,使用 Taro 3 并不意味着我们必须牺牲应用的性能。事实上,Taro 已经提供了一系列的性能优化方法,并且不断探索更加极致的优化方案。
本文将为大家提供一些小程序开发的最佳实践,帮助大家最大程度地提升小程序应用的性能表现。
一、如何提升初次渲染性能
如果初次渲染的数据量非常大,可能会导致页面在加载过程中出现一段时间的白屏。为了解决这个问题,Taro 提供了预渲染功能(Prerender)。
使用 Prerender 非常简单,只需在项目根目录下的 config
文件夹中找到 index.js/dev.js/prod.js
三者中的任意一个项目配置文件,并根据项目情况进行修改。在编译时,Taro CLI 会根据你的配置自动启动预渲染功能。
const config = {
...
mini: {
prerender: {
match: 'pages/shop/**', // 所有以 `pages/shop/` 开头的页面都参与 prerender
include: ['pages/any/way/index'], // `pages/any/way/index` 也会参与 prerender
exclude: ['pages/shop/index/index'] // `pages/shop/index/index` 不用参与 prerender
}
}
};
module.exports = config
更详细说明请参考官方文档: https://taro-docs.jd.com/docs/prerender
二、如何提升更新性能
由于 Taro 使用小程序的 template
进行渲染,这会引发一个问题:所有的 setData
更新都需要由页面对象调用。当页面结构较为复杂时,更新的性能可能会下降。
当层级过深时,setData
的数据结构如下:
page.setData({
'root.cn.[0].cn.[0].cn.[0].cn.[0].markers': [],
})
期望的 setData
数据结构:
component.setData({
'cn.[0].cn.[0].markers': [],
})
目前有两种方法可以实现上述结构,以实现局部更新的效果,从而提升更新性能:
1. 全局配置项 baseLevel
对于不支持模板递归的小程序(例如微信、QQ、京东小程序等),当 DOM 层级达到一定数量后,Taro 会利用原生自定义组件来辅助递归渲染。简单来说,当 DOM 结构超过 N 层时,Taro 将使用原生自定义组件进行渲染(可以通过修改配置项 baseLevel
来调整 N 的值,建议设置为 8 或 4)。
需要注意的是,由于这是全局设置,可能会带来一些问题,例如:
- 在跨原生自定义组件时,
flex
布局会失效(这是影响最大的问题); - 在
SelectorQuery.select
方法中,跨自定义组件的后代选择器写法需要增加>>>:.the-ancestor >>> .the-descendant
。
2. 使用 CustomWrapper 组件
CustomWrapper
组件的作用是创建一个原生自定义组件,用于调用后代节点的 setData
方法,以实现局部更新的效果。
我们可以使用它来包裹那些遇到更新性能问题的模块,例如:
import { View, Text } from '@tarojs/components'
export default function () {
return (
<View className="index">
<Text>Demo</Text>
<CustomWrapper>
<GoodsList />
</CustomWrapper>
</View>
)
}
三、如何提升长列表性能
长列表是常见的组件,当生成或加载的数据量非常大时,可能会导致严重的性能问题,尤其在低端机上可能会出现明显的卡顿现象。
为了解决长列表的问题,Taro 提供了 VirtualList
组件和 VirtualWaterfall
组件。它们的原理是只渲染当前可见区域(Visible Viewport)的视图,非可见区域的视图在用户滚动到可见区域时再进行渲染,以提高长列表滚动的流畅性。
1. VirtualList 组件(虚拟列表)
以 React Like 框架使用为例,可以直接引入组件:
import VirtualList from '@tarojs/components/virtual-list'
一个最简单的长列表组件如下所示:
function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}
const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>
)
})
export default class Index extends Component {
state = {
data: buildData(0),
}
render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualList
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件,这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>
)
}
}
更多详情可以参考官方文档: https://taro-docs.jd.com/docs/virtual-list
2. VirtualWaterfall 组件(虚拟瀑布流)
以 React Like 框架使用为例,可以直接引入组件:
import { VirtualWaterfall } from `@tarojs/components-advanced`
一个最简单的长列表组件如下所示:
function buildData(offset = 0) {
return Array(100)
.fill(0)
.map((_, i) => i + offset)
}
const Row = React.memo(({ id, index, data }) => {
return (
<View id={id} className={index % 2 ? 'ListItemOdd' : 'ListItemEven'}>
Row {index} : {data[index]}
</View>
)
})
export default class Index extends Component {
state = {
data: buildData(0),
}
render() {
const { data } = this.state
const dataLen = data.length
return (
<VirtualWaterfall
height={800} /* 列表的高度 */
width="100%" /* 列表的宽度 */
item={Row} /* 列表单项组件,这里只能传入一个组件 */
itemData={data} /* 渲染列表的数据 */
itemCount={dataLen} /* 渲染列表的长度 */
itemSize={100} /* 列表单项的高度 */
/>
)
}
}
更多详情可以参考官方文档: https://taro-docs.jd.com/docs/virtual-waterfall
四、如何避免 setData 数据量较大
众所周知,对小程序性能的影响较大的主要有两个因素,即 setData
的数据量和单位时间内调用 setData
函数的次数。在 Taro 中,会对 setData
进行批量更新操作,因此通常只需要关注 setData
的数据量大小。下面通过几个例子来说明如何避免数据量过大的问题:
例子 1:删除楼层节点要谨慎处理
目前 Taro 在处理节点删除方面存在一些缺陷。假设存在以下代码写法:
<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
{isShowModal && <Modal />}
</View>
当 isShowModal
从 true
变为 false
时,模态弹窗会消失。此时,Modal
组件的兄弟节点都会被更新,setData
的数据是 Slider + Goods
组件的 DOM 节点信息。
一般情况下,这不会对性能产生太大影响。然而,如果待删除节点的兄弟节点的 DOM 结构非常复杂,比如一个个楼层组件,删除操作的副作用会导致 setData
的数据量变大,从而影响性能。
为了解决这个问题,可以通过隔离删除操作来进行优化。
<View>
<!-- 轮播 -->
<Slider />
<!-- 商品组 -->
<Goods />
<!-- 模态弹窗 -->
<View>
{isShowModal && <Modal />}
</View>
</View>
例子 2:基础组件的属性要保持引用
当基础组件(例如 View
、Input
等)的属性值为非基本类型时,假设存在以下代码写法:
<Map
latitude={22.53332}
longitude={113.93041}
markers={[
{
latitude: 22.53332,
longitude: 113.93041,
},
]}
/>
每次渲染时,React 会对基础组件的属性进行浅比较。如果发现 markers
的引用不同,就会触发组件属性的更新。这最终导致了 setData
操作的频繁执行和数据量的增加。 为了解决这个问题,可以使用状态(state
)或闭包等方法来保持对象的引用,从而避免不必要的更新。
<Map
latitude={22.53332}
longitude={113.93041}
markers={this.state.markers}
/>
五、更多最佳实践
1. 阻止滚动穿透
在小程序开发中,当存在滑动蒙层、弹窗等覆盖式元素时,滑动事件会冒泡到页面上,导致页面元素也会跟着滑动。通常我们会通过设置 catchTouchMove
来阻止事件冒泡。
然而,由于 Taro3 事件机制的限制,小程序事件都是以 bind
的形式进行绑定。因此,与 Taro1/2 不同,调用 e.stopPropagation()
并不能阻止滚动事件的穿透。
解决办法 1:使用样式(推荐)
可以为需要禁用滚动的组件编写以下样式:
{
overflow:hidden;
height: 100vh;
}
解决办法 2:使用 catchMove
对于极个别的组件,比如 Map
组件,即使使用样式固定宽高也无法阻止滚动,因为这些组件本身具有滚动的功能。因此,第一种方法无法处理冒泡到 Map
组件上的滚动事件。 在这种情况下,可以为 View
组件添加 catchMove
属性:
// 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
<View catchMove />
2. 跳转预加载
在小程序中,当调用 Taro.navigateTo
等跳转类 API 后,新页面的 onLoad
事件会有一定的延时。因此,为了提高用户体验,可以将一些操作(如网络请求)提前到调用跳转 API 之前执行。
对于熟悉 Taro 的开发者来说,可能会记得在 Taro 1/2 中有一个名为 componentWillPreload
的钩子函数。然而,在 Taro 3 中,这个钩子函数已经被移除了。不过,开发者可以使用 Taro.preload()
方法来实现跳转预加载的效果:
// pages/index.js
Taro.preload(fetchSomething())
Taro.navigateTo({ url: '/pages/detail' })
// pages/detail.js
console.log(getCurrentInstance().preloadData)
3. 建议把 Taro.getCurrentInstance() 的结果保存下来
在开发过程中,我们经常会使用 Taro.getCurrentInstance()
方法来获取小程序的 app
、page
对象以及路由参数等数据。然而,频繁地调用该方法可能会导致一些问题。
因此,建议将 Taro.getCurrentInstance()
的结果保存在组件中,并在需要时直接使用,以避免频繁调用该方法。这样可以提高代码的执行效率和性能。
class Index extends React.Component {
inst = Taro.getCurrentInstance()
componentDidMount() {
console.log(this.inst)
}
}
六、预告:小程序编译模式(CompileMode)
Taro 一直追求并不断突破性能的极限,除了以上提供的最佳实践,我们即将推出小程序编译模式(CompileMode)。
什么是 CompileMode?
前面已经说过,Taro3 是一种重运行时的框架,当节点数量增加到一定程度时,渲染性能会显著下降。 因此,为了解决这个问题,Taro 引入了 CompileMode 编译模式。
CompileMode 在编译阶段对开发者的代码进行扫描,将 JSX
和 Vue template
代码提前编译为相应的小程序模板代码。这样可以减少小程序渲染层虚拟 DOM 树节点的数量,从而提高渲染性能。 通过使用 CompileMode,可以有效减少小程序的渲染负担,提升应用的性能表现。
如何使用?
开发者只需为小程序的基础组件添加 compileMode
属性,该组件及其子组件将会被编译为独立的小程序模板。
function GoodsItem () {
return (
<View compileMode>
...
</View>
)
}
目前第一阶段的开发工作已经完成,我们即将发布 Beta 版本,欢迎大家关注! 想提前了解的可以查看 RFC 文档: https://github.com/NervJS/taro-rfcs/blob/feat/compile-mode/rfcs/0000-compile-mode.md
结尾
通过采用 Taro 的最佳实践,我们相信您的小程序应用性能一定会有显著的提升。未来,我们将持续探索更多优化方案,覆盖更广泛的应用场景,为开发者提供更高效、更优秀的开发体验。
如果您在项目中有任何经验总结或思考,欢迎向我们投稿并进行交流,让我们一起分享给更多开发者,非常感谢您的支持!
作者:京东零售 利齐诺
来源:京东云开发者社区 转载请注明来源
Taro:高性能小程序的最佳实践的更多相关文章
- 微信小程序自动化测试最佳实践(附 Python 源码)
本文为霍格沃兹测试学院测试大咖公开课<微信小程序自动化测试>图文整理精华版. 随着微信小程序的功能和生态日益完善,很多公司的产品业务形态逐渐从 App 延升到微信小程序.微信公众号等.小程 ...
- Taro:使用taro完成小程序开发
前言:taro是一个可以很好实现一次开发,多端统一的框架,本文只介绍它小程序端开发的一些内容.小程序项目搭建gitup已经有很清楚的说明:https://github.com/NervJS/taro ...
- Taro -- 微信小程序wxParse达到html转换wxml
Taro微信小程序可以用wxParse来达到html转换wxml的效果:https://github.com/NervJS/taro-components-test/blob/master/src/p ...
- Taro -- 微信小程序登录
Taro微信小程序登录 1.调用Taro.login()获取登录凭证code: 2.调用Taro.request()将code传到服务器: 3.服务器端调用微信登录校验接口(appid+appsecr ...
- 【转】优化Web程序的最佳实践
自动排版有点乱,看着蛋疼,建议下载中文PDF版阅读或阅读英文原文. Yahoo!的Exceptional Performance团队为改善Web性能带来最佳实践.他们为此进行了 一系列的实验.开发了各 ...
- Taro 微信小程序 上传文件到minio
小程序前端上传文件不建议直接引用minio的js npm包,一来是这个包本身较大,会影响小程序的体积,二来是ak sk需要放到前端存储,不够安全,因此建议通过请求后端拿到签名数据后上传. 由于小程序的 ...
- WePY - 小程序敏捷开发实践丨掘金开发者大会
声明:内容转载他处,如有侵权,可协商下架 本主题虽然在其它地方讲了很多次,但还是有非常多新内容.因为很多东西正在做或者想要做.本次分享主要分为以下几个部分: WePY 的介绍 WePY 的用户 上面展 ...
- 小程序webview应用实践
原文:小程序webview实践 作者:张所勇(转转开放业务部前端负责人) 公众号:大转转FE Fundebug经授权转载,版权归原作者所有. 大家好,我是转转开放业务部前端负责人张所勇,今天主要来跟大 ...
- Taro开发小程序移动地图固定中间获取地址
效果图如下: 绿色地标固定在中间,移动地图的时候获取对应信息. 先定义map. <Map className="location" id={location} latitud ...
- taro 微信小程序原生作用域获取
在 Taro 的页面和组件类中,this 指向的是 Taro页面或组件实例. 但是一般我们需要获取 Taro的页面和组件 所对应的 小程序原生页面和组件实例,这个时候我们可以通过 this.$scop ...
随机推荐
- nodejs中事件循环机制与面试题详解
nodejs中架构如下图所示,通过v8引擎来执行js代码,通过中间层 libuv 来读写文件系统.网络等做一些操作. nodejs中提供阻塞和非阻塞的调用方式,比如fs模块中读取文件,可以根据 ...
- html5 3.0 表单
表单的定义:多个输入框,以表格的形式展示 表单常用在网页登录和注册功能中 表单的元素属性:<input type="text"name=" "valu ...
- 9、Spring之代理模式
9.1.环境搭建 9.1.1.创建module 9.1.2.选择maven 9.1.3.设置module名称和路径 9.1.4.module初始状态 9.1.5.配置打包方式和依赖 <?xml ...
- 论文解读(WIND)《WIND: Weighting Instances Differentially for Model-Agnostic Domain Adaptation》
Note:[ wechat:Y466551 | 可加勿骚扰,付费咨询 ] 论文信息 论文标题:WIND: Weighting Instances Differentially for Model-Ag ...
- 快速理解DDD领域驱动设计架构思想-基础篇
1 前言 本文与大家一起学习并介绍领域驱动设计(Domain Drive Design) 简称DDD,以及为什么我们需要领域驱动设计,它有哪些优缺点,尽量用一些通俗易懂文字来描述讲解领域驱动设计,本篇 ...
- minio 支持object搜索方案
minio支持上传时对object打标签,查询时可以根据标签做筛选.但是有ftp上传文件的需求,导致无法给object打标签.并且也不清楚minio对于根据标签的筛选性能如何,因此我们打算将objec ...
- Vue源码学习(八):生命周期调用
好家伙, Vue源码学习(七):合并生命周期(混入Vue.Mixin) 书接上回,在上一篇中,我们已经实现了合并生命周期 现在,我们要在我们的初始化过程中,注册生命周期 1.项目目录 红框为本篇 ...
- OSPF常用配置和常用的查看命令
转载请注明出处: 1.启动OSPF进程,进入OSPF视图. [Huawei] ospf [ process-id | Router ID Router ID ] 路由器支持OSPF多进程,进程号是本地 ...
- BizSpring在线商城常见问题
一.什么是BizSpring在线商城? BizSpring在线商城是一个用java语言开发的完全开源的网络商城平台.该项目已经经历多次迭代升级是一个的成熟的在线商城解决方案,它具有轻量级,易于维护,操 ...
- centos7 oracle11gR2安装
CentOS7安装Oracle 11gR2 图文详解 摘自: http://www.linuxidc.com/Linux/2016-04/130559.htm 最近要运维一个项目,准备在家办公,公司无 ...