Skip to content

Latest commit

 

History

History
403 lines (294 loc) · 12.6 KB

optimized.mdx

File metadata and controls

403 lines (294 loc) · 12.6 KB
title
小程序性能优化指南

import { ReactIcon, VueIcon } from '@site/static/icons' import TabItem from '@theme/TabItem' import Tabs from '@theme/Tabs'

Taro 3 为了兼容 React、Vue 等 Web 开发框架,在运行时对浏览器环境进行模拟,实现了 BOM 和 DOM 等一系列 API。相比 Taro 1/2,Taro 3 是一款重运行时、轻编译时的框架,在性能方面会有一定损耗。因此 Taro 3 提供了一系列的性能优化手段,从而提升 Taro 3 应用的性能。

半编译模式 —— CompileMode

:::info Taro v3.6.23 开始支持,目前只支持 React,暂不支持 Vue。底层实现原理请参考 RFC 文档。 :::

在节点数量增多到一定量级时,Taro3 的渲染性能会大幅下降,出现白屏时间长、交互延时等问题。经排查发现是目前 Taro 的 <template> 模板语法所造成的,为此我们参考 Taro 1/2 的思路,提供了 CompileMode 渲染模式。CompileMode 适合长列表 Item 这类会被重复渲染多次的组件使用,在长列表场景能提升 30% 以上的首开速度,同时能有效减少节点过多时产生的交互延时问题。CompileMode 可以说是应对复杂页面性能优化的“银弹”。

使用方法

首先在 Taro 编译配置中开启使用半编译模式:

const config = {
  mini: {
    experimental: {
      compileMode: true
    }
  }
  // ...
}

然后只需要给 Taro 基础组件添加 compileMode 属性,该组件及其 children 将会被编译为单独的小程序模板:

function GoodsItem () {
  return (
    <View compileMode>
      ...
    </View>
  )
}

常见问题

1. 编译出的模板文件会增加包体积

半编译模式使用了空间来换时间,编译出模板会令包体积增大。增加的文件大小视 JSX 写法而定,可以在编译后的页面目录下找到对应的模板文件,如 pages/index/index.jsx 编译出的模板位置在 dist/pages/index/index-templates.wxml。因此开发者应权衡后使用。

2. 只能优化部分语法

编译阶段只能识别、优化部分语法,不支持的语法会自动回退到 Taro3 默认的渲染模式,具体支持的语法可以查阅 RFC 文档

有一种常见语法需要注意:编译阶段只能识别 Taro 基础组件,而 React、Vue 组件的渲染会自动回退到旧的渲染模式。如果这些 React、Vue 组件也需要使用半编译模式,需要在组件内部再次添加 compileMode 属性:

function Index () {
  return (
    <View compileMode>
      <Text>Hello</Text> {/* 能被编译阶段识别 */}
      <Foo /> {/*会自动回退到 Taro3 默认的渲染模式*/}
    </View>
  )
}

function Foo () {
  return (
    // 如果希望 Foo 组件也使用半编译模式,需要在 Foo 组件内部再次添加 compileMode 属性
    <View compileMode>
      ...
    </View>
  )
}

使用 WXS

:::info Taro v3.6.25 开始支持。目前只支持在 Taro React 小程序中使用,暂不支持 Taro Vue,暂不支持在其他端使用。 :::

借助 CompileMode,Taro 可以支持使用各类小程序的视图层脚步语言,如微信小程序的 wxs、支付宝小程序的 sjs、京东小程序的 jds、头条小程序的 sjs 等。

用法:

import { View, ScrollView, Script } from '@tarojs/components'

export default function Index() {
  return (
    {/* 首先需要开启编译模式 */}
    <View compileMode>

      {/* <Script> 组件等于小程序 <wxs> 组件,需要填写 src 和 module 属性,详情请参考小程序官方文档 */}
      <Script src='./animation.wxs' module='ani'></Script>

      {/* 访问 WXS 的导出值 */}
      <View>{ani.msg}</View>
      <View>{ani.getMsg()}</View>
      <View hoverClass={ani.hoverClass}></View>
      <View hoverClass={ani.getHoverClass()}></View>

      {/* WXS 响应事件 */}
      <ScrollView onScrollToUpper={ani.toupper}>
        <View onTouchStart={ani.touchstart} onTouchMove={ani.touchmove} onTouchEnd={ani.touchend}></View>
      </ScrollView>
    </View>
  )
}

优化初次渲染性能

:::tip 一般情况下不需要开启,请根据实际情况进行使用。 :::

当初次渲染的数据量非常大时,可能会导致页面白屏一段时间。因此 Taro 提供了预渲染功能来解决此问题。

优化更新性能

:::tip 经验证明,此项优化能大大减少 Taro3 的更新卡顿问题,尤其是在低端机上。 :::

Taro3 使用小程序的 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 层后,会使用原生自定义组件进行渲染。N 默认是 16 层,可以通过修改配置项 baseLevel 修改 N。

baseLevel 设置为 8 甚至 4 层,能非常有效地提升更新时的性能。但是设置是全局性的,会带来若干问题:

  1. flex 布局在跨原生自定义组件时会失效,这是影响最大的一个问题。
  2. SelectorQuery.select 方法的跨自定义组件的后代选择器写法需要增加 >>>.the-ancestor >>> .the-descendant

2. CustomWrapper 组件

为了解决全局配置不灵活的问题,我们增加了一个基础组件 CustomWrapper。它的作用是创建一个原生自定义组件,对后代节点的 setData 将由此自定义组件进行调用,达到局部更新的效果。

开发者可以使用它去包裹遇到更新性能问题的模块,提升更新时的性能。因为 CustomWrapper 组件需要手动使用,开发者能够清楚“这层使用了自定义组件,需要避免自定义组件的两个问题”。

<Tabs defaultValue="React" values={[ {label: , value: 'React'}, {label: , value: 'Vue'} ]}>

import { View, Text } from '@tarojs/components'

export default function () {
  return (
    <View className="index">
      <Text>Demo</Text>
      <CustomWrapper>
        <GoodsList />
      </CustomWrapper>
    </View>
  )
}
<template>
  <view class="index">
    <text>Demo</text>
    <custom-wrapper>
      <GoodsList />
    </custom-wrapper>
  </view>
</template>

<script>
  import GoodsList from '@/components/goods-list'

  export default {
    components: {
      GoodsList,
    },
  }
</script>

优化长列表性能

针对长列表的场景,Taro 提供了 VirtualList 组件辅助开发者进行优化。

只会渲染当前可视区域内的组件,非可视区域的组件将会在用户滚动到可视区域内后再渲染,从而减少实际渲染的组件、优化渲染性能。

跳转预加载

:::info Taro 1 / 2 中提供的 componentWillPreload 钩子在 Taro 3 中已废弃。 :::

在小程序中,从调用 Taro.navigateTo 等路由跳转 API 后,到小程序页面触发 onLoad 会有一定延时,因此一些网络请求可以提前到发起跳转的前一刻去请求。

Taro 3 提供了 Taro.preload API,可以把需要预加载的内容作为参数传入,然后在新页面加载后通过 Taro.getCurrentInstance().preloadData 获取到预加载的内容。

例子:

<Tabs defaultValue="React" values={[ {label: , value: 'React'}, {label: , value: 'Vue'} ]}>

// A 页面
// 调用跳转方法前使用 Taro.preload
Taro.preload(fetchSomething())
Taro.navigateTo({ url: '/pages/B/B' })

// B 页面
componentWillMount () {
  console.log(Taro.getCurrentInstance().preloadData)
}
// A 页面
// 调用跳转方法前使用 Taro.preload
Taro.preload(fetchSomething())
Taro.navigateTo({ url: '/pages/B/B' })

// B 页面
mounted () {
  console.log(Taro.getCurrentInstance().preloadData)
}

<Tabs defaultValue="React" values={[ {label: , value: 'React'}, {label: , value: 'Vue'} ]}>

// A 页面
Taro.preload('x', 1)
Taro.navigateTo({ url: '/pages/B/B' })

// B 页面
componentWillMount () {
  console.log(Taro.getCurrentInstance().preloadData)
}
// A 页面
Taro.preload('x', 1)
Taro.navigateTo({ url: '/pages/B/B' })

// B 页面
mounted () {
  console.log(Taro.getCurrentInstance().preloadData)
}

写法最佳实践

对小程序的性能影响较大的有两个因素,分别是 setData数据量和单位时间 setData 函数的调用次数

当遇到性能问题时,在项目中打印 setData 的数据将非常有利于帮助定位问题。开发者可以通过进入 Taro 项目的 dist/taro.js 文件,搜索定位 .setData 的调用位置,然后对数据进行打印。

在 Taro 中,会对 setDatabatch 捆绑更新操作,因此更多时候只需要考虑 setData 的数据量大小问题。

以下是我们梳理的开发者需要注意的写法问题:

1. 删除楼层节点要谨慎处理

假设有一种这样一种结构:

<View>
  <!-- 轮播 -->
  <Slider />
  <!-- 商品组 -->
  <Goods />
  <!-- 模态弹窗 -->
  {isShowModal && <Modal />}
</View>

Taro3 目前对节点的删除处理是有缺陷的。当 isShowModaltrue 变为 false 时,模态弹窗会从消失。此时 Modal 组件的兄弟节点都会被更新,setData 的数据是 Slider + Goods 组件的 DOM 节点信息。

一般情况下,影响不会太大,开发者无须由此产生心智负担。但倘若待删除节点的兄弟节点的 DOM 结构非常复杂,如一个个楼层组件,删除操作的副作用会导致 setData 数据量较大,从而影响性能。

解决办法:

目前我们可以这样优化,隔离删除操作:

<View>
  <!-- 轮播 -->
  <Slider />
  <!-- 商品组 -->
  <Goods />
  <!-- 模态弹窗 -->
  <View>
    {isShowModal && <Modal />}
  </View>
</View>

我们正在对删除节点的算法进行优化,完全规避这种不必要的 setData,于 v3.1 推出。

2. 基础组件的属性要保持引用

React

假设基础组件(如 ViewInput 等)的属性值为非基本类型时,尽量保持对象的引用。

假设有以下写法:

<Map
  latitude={22.53332}
  longitude={113.93041}
  markers={[
    {
      latitude: 22.53332,
      longitude: 113.93041,
    },
  ]}
/>

每次渲染时,React 会对基础组件的属性做浅对比,这时发现 markers 的引用不同,就会去更新组件属性。最后导致 setData 次数增多、setData 数据量增大。

解决办法:

可以通过 state、闭包等手段保持对象的引用:

<Map latitude={22.53332} longitude={113.93041} markers={this.state.markers} />

3. 基础组件不要挂载额外属性

基础组件(如 ViewInput 等)如若设置了非标准的属性,目前这些额外属性会被一并进行 setData,而实际上小程序并不会理会这些属性,所以 setData 的这部分数据是冗余的。

例如 Text 组件的标准属性有 selectableuser-selectspace decode 四个,如果我们为它设置一个额外属性 something,那么这个额外的属性也是会被 setData。

<Text something="extra" />

实战文章