uni-app 性能优化实战:分包加载、首屏提速与长列表渲染

uni-app 性能优化实战:分包加载、首屏提速与长列表渲染

admin
2026-07-17 / 0 评论 / 6 阅读

小程序的性能红线比 Web 苛刻得多:主包 2MB 上限、启动加载有评分、setData 频繁会掉帧。本文围绕包体积、启动速度、运行时性能三个维度,给出可直接落地的优化清单。

一、包体积优化:分包是第一要务

为什么必须分包

微信小程序限制:主包 ≤ 2MB,单个分包 ≤ 2MB,总包 ≤ 30MB(普通分包)。业务做多了,主包必然爆。分包加载(subPackages)把非首屏页面拆出去,用户进入对应模块时才下载。

分包配置

// pages.json
{
  "pages": [
    "pages/index/index",
    "pages/order/list"
  ],
  "subPackages": [
    {
      "root": "subpkg-order",
      "pages": [
        "pages/detail/detail",
        "pages/refund/refund",
        "pages/invoice/invoice"
      ]
    },
    {
      "root": "subpkg-member",
      "pages": [
        "pages/coupon/coupon",
        "pages/points/points"
      ]
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["subpkg-order"]
    },
    "pages/order/list": {
      "network": "wifi",
      "packages": ["subpkg-member"]
    }
  }
}

目录结构相应调整:

src/
├── pages/              # 主包:只放首屏路径
├── subpkg-order/
│   └── pages/
│       ├── detail/
│       ├── refund/
│       └── invoice/
├── subpkg-member/
│   └── pages/
├── static/             # 只放主包用的静态资源
└── components/

分包的两个关键规则

  1. 分包页面跳转用完整路径:uni.navigateTo({ url: '/subpkg-order/pages/detail/detail?id=1' })
  2. 分包可以引用主包的组件/工具,反之不行(主包不能 import 分包内资源,否则等于没拆)

preloadRule:分包预下载

上面配置里用户在首页时,预下载订单分包(全网络),进入订单列表时预下载会员分包(仅 WiFi)。配置得当能把"进入子模块的白屏时间"压到无感。

静态资源治理

主包 2MB 里最容易被图片吃掉:

  • 图片 CDN 化:static 目录里只留 tab 图标等必须本地的资源,其余全部走远程 URL
  • 压缩:tinypng 压一遍,通常能砍 60%+
  • 按需引入组件库:uni-ui 等支持 easycom 按需引入,但要确认 tree-shaking 生效;uview-plus 建议用 its 按需加载配置
// vite.config.js 分析包体积
import { visualizer } from 'rollup-plugin-visualizer'

export default {
  plugins: [visualizer({ filename: 'stats.html' })]
}

构建后打开 stats.html,哪些模块占了体积一目了然。

二、启动性能:首屏体验

启动流程与耗时构成

小程序启动 = 下载代码包 → 初始化 → 页面首次渲染。开发者能控制的是初始化阶段的同步任务量首屏依赖的数据请求

优化一:App.onLaunch 里别做重活

// App.vue
export default {
  onLaunch() {
    // ❌ 常见错误:启动时同步拉一堆配置
    // this.initConfig() // 200ms+ 的同步阻塞

    // ✅ 延迟到首页渲染后再做
    // this.getConfigId = setTimeout(() => this.initConfig(), 0)
  }
}

启动阶段每个同步 API 调用、每个 await 都直接推迟首屏。原则:onLaunch 只做必须的(登录态检查),其余全部后置

优化二:骨架屏

// pages.json
{ "path": "pages/index/index", "style": { "navigationStyle": "custom" } }

HBuilderX 可以把页面骨架生成微信快照。手写方案是在数据未到时渲染结构占位:

<template>
  <view>
    <template v-if="loaded">
      <view v-for="item in list" :key="item.id" class="goods-card">
        <image :src="item.cover" />
        <text>{{ item.title }}</text>
      </view>
    </template>
    <template v-else>
      <view v-for="i in 4" :key="i" class="goods-card skeleton">
        <view class="skeleton-img" />
        <view class="skeleton-line" />
      </view>
    </template>
  </view>
</template>

<style>
.skeleton-img, .skeleton-line {
  background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%);
  background-size: 200% 100%;
  animation: shimmer 1.2s infinite;
}
@keyframes shimmer { to { background-position: -200% 0; } }
</style>

优化三:初始渲染缓存

微信端开启后,第二次打开直接展示上次渲染的快照,再用新数据更新:

{ "path": "pages/index/index", "style": { "initialRenderingCache": "static" } }

三、运行时性能:setData 与长列表

理解 setData 的成本

uni-app 编译到小程序后,逻辑层和渲染层分离,每次响应式数据变化都会产生一次 setData 通信。数据量大、频率高时,通信本身成为瓶颈。

规则一:不要频繁 setData 小数据

// ❌ 倒计时每秒 setData 一次整个对象
this.timer = setInterval(() => {
  this.leftSeconds-- // 对象级更新
}, 1000)

// ✅ 高频更新只改必要字段,或者干脆用 wx 层优化
// 页面数据结构上把高频变化的字段隔离出来

规则二:长列表分页 + 局部更新

<script>
export default {
  data() {
    return {
      list: [],
      page: 1,
      finished: false,
      loading: false
    }
  },
  onReachBottom() {
    this.loadMore()
  },
  methods: {
    async loadMore() {
      if (this.loading || this.finished) return
      this.loading = true
      try {
        const { records, hasMore } = await this.$api.getOrders({
          page: this.page + 1
        })
        // 追加而不是替换,配合 :key 复用节点
        this.list.push(...records)
        this.page++
        this.finished = !hasMore
      } finally {
        this.loading = false
      }
    }
  }
}
</script>

长列表的进阶方案是虚拟列表:只渲染可视区域附近的节点。长列表组件(如 z-paging、uview 的 u-list)内置了该能力:

<z-paging
  v-model="list"
  @query="queryList"
  :default-page-size="20"
  use-virtual-list
  use-inner-scroll
>
  <view v-for="(item, index) in list" :key="item.id">
    <text>{{ item.title }}</text>
  </view>
</z-paging>

上千条数据滚动如丝般顺滑的核心:滚动容器内只保留视口 ± 缓冲区内的真实节点,用撑高的占位元素维持滚动条

规则三:图片懒加载

<image :src="item.cover" lazy-load mode="aspectFill" />

原生 image 组件的 lazy-load 让图片进入视口前后才加载,列表页流量和渲染压力立减。

规则四:避免大对象进 data

不变的数据(城市列表、配置表)放 this 挂载而非 data,避免进入响应式系统产生 setData 开销:

// ❌ 一万条城市数据放 data,每次任意 setData 都可能全量传输
// ✅ 非渲染数据脱离响应式
created() {
  this.allCities = cityData // 不进 data,只做查找用
}

四、优化检查清单

上线前对照过一遍:

包体积

  • [ ] 主包 < 1.5MB(留余量)
  • [ ] 非首屏页面全部拆分包
  • [ ] preloadRule 覆盖高频跳转路径
  • [ ] static 目录无超过 50KB 的图片
  • [ ] stats.html 确认无意外的大依赖

启动

  • [ ] onLaunch 只留登录态检查
  • [ ] 首页有骨架屏或初始渲染缓存
  • [ ] 首屏接口合并,首屏数据 < 3 个请求

运行时

  • [ ] 长列表分页 + 虚拟列表
  • [ ] image 全部 lazy-load + 合适的 mode
  • [ ] 高频更新字段做隔离
  • [ ] 定时器在 onHide 暂停、onUnload 清除
  • [ ] 真机(低端安卓)过一遍核心页面流畅度

五、工具

  • 微信开发者工具 → 审计:代码依赖分析、包体积构成
  • 体验评分:自动检测常见性能问题
  • 真机调试 Performance 面板:setData 频率和耗时可视化

优化永远要以数据为依据:先跑一遍体验评分和真机 profile,找到真实瓶颈再动手,而不是凭感觉瞎改。

总结

  • 分包是包体积问题的根本解,preloadRule 消除分包切换白屏
  • 启动性能核心是砍掉 onLaunch 的同步任务,骨架屏改善体感
  • setData 通信是小程序性能的第一杀手:合并更新、隔离高频字段、大对象不进 data
  • 千级长列表必须上虚拟列表
  • 一切优化先 profile 后动手

性能优化做到位的小程序,启动快、滚动顺、耗电少,用户体验评分和留存都会直观反映出来。

0

评论 (0)

取消
0:00