为什么选uni-app:百万日活电商项目的跨端选型复盘
那年双十一前夕,我们团队接到了一个紧急需求:在原有微信小程序的基础上,两周内上线支付宝和抖音小程序版本。当时我们的电商小程序日活已经突破120万,商品SKU超过5万个,订单系统每天处理近10万笔交易。面对这种体量的项目,技术选型直接决定了项目能否按期交付以及后续维护成本。
我们当时评估了三种方案:原生开发、Taro和uni-app。原生开发需要为每个平台配备独立团队,人力成本直接翻三倍;Taro虽然也能跨端,但在我们实际测试中发现,对于复杂列表页面的渲染性能不如预期,特别是在抖音小程序上出现了明显的卡顿。最终我们选择uni-app 3.9.8版本,主要基于几个实际考量。
跨端编译能力是最关键的因素。我们那个订单列表页有十几种状态组合,如果用原生开发,每个平台都要写一遍逻辑。uni-app的条件编译让我们能保留90%的共用代码,只在支付接口、分享逻辑等差异处用#ifdef处理。比如抖音小程序的支付流程和微信完全不同,我们这样处理:
// pages/order/checkout.vue
<template>
<view>
<button @click="handlePay">立即支付</button>
</view>
</template>
<script setup lang="ts">
const handlePay = () => {
// #ifdef MP-WEIXIN
wx.requestPayment({
timeStamp: '',
nonceStr: '',
package: '',
signType: 'RSA',
paySign: '',
success: () => {
console.log('微信支付成功')
}
})
// #endif
// #ifdef MP-TOUTIAO
tt.pay({
orderInfo: {
order_id: '123456',
amount: 100
},
success: () => {
console.log('抖音支付成功')
}
})
// #endif
}
</script>
性能方面,我们的商品详情页有长达50屏的图文混排内容。最初用vue页面时,滚动到20屏左右就开始掉帧,内存占用达到180MB。切换到nvue原生渲染后,同样的内容内存降到110MB,滚动帧率稳定在55fps以上。这个差异在低端安卓机上特别明显,用户投诉"页面卡死"的数量减少了70%。
另一个实际好处是生态插件。我们接入uniPay支付插件时,发现它已经封装了微信、支付宝、银联等十几种支付方式,省去了我们单独对接每个支付渠道的时间。特别是处理微信小程序的分账功能时,官方插件直接提供了完整的分账接口调用示例,我们两天就完成了原本预计一周的对接工作。
当然也有取舍。uni-app的vue页面在复杂动画场景下性能不如原生,我们后来把首页的秒杀动画模块改用nvue实现。还有一次线上问题,我们发现抖音小程序的分享卡片生成逻辑和微信不同,排查了半天才发现是平台API差异,最后通过条件编译单独处理才解决。
Vue 3 + TS实战:重构订单模块的性能对比与踩坑记录
今年3月,我们把订单模块从Vue 2迁移到Vue 3 + TypeScript,整个过程持续了三周。选择升级不是赶时髦,而是实实在在遇到了性能瓶颈。在大促期间,我们的订单列表页加载时间从平时的300ms飙升到800ms,用户取消订单率增加了15%。监控显示主要是响应式数据更新导致的性能问题。
Vue 3的编译优化确实明显。我们实测在相同数据量下,订单列表的渲染耗时从420ms降到280ms。特别是使用组合式API后,逻辑复用变得清晰很多。以前用mixin实现的订单状态判断,现在可以提取成独立的composable:
// composables/useOrderStatus.ts
import { ref, computed } from 'vue'
export function useOrderStatus(order: Ref<OrderItem>) {
const statusText = computed(() => {
if (order.value.isPaid && !order.value.isShipped) return '待发货'
if (order.value.isShipped && !order.value.isReceived) return '待收货'
return '已完成'
})
const canCancel = computed(() => {
return !order.value.isPaid && !order.value.isCancelled
})
return {
statusText,
canCancel
}
}
// 在组件中使用
import { useOrderStatus } from '@/composables/useOrderStatus'
const order = ref<OrderItem>({} as OrderItem)
const { statusText, canCancel } = useOrderStatus(order)
TypeScript带来的类型安全帮我们避免了至少三次线上事故。有一次后端接口返回的金额字段从string改成了number,如果没有TS类型检查,这个变更可能直接引发前端计算错误。现在我们的订单接口都有完整的类型定义:
// types/order.d.ts
export interface OrderItem {
id: string
orderNo: string
amount: number
status: OrderStatus
products: ProductItem[]
createTime: number
payTime?: number
}
export type OrderStatus = 'pending' | 'paid' | 'shipped' | 'received' | 'cancelled'
// 接口调用时的类型检查
const fetchOrderList = async (): Promise<OrderItem[]> => {
const res = await uni.request({
url: '/api/order/list',
method: 'GET'
})
return res.data as OrderItem[]
}
升级过程中最头疼的是响应式系统的差异。Vue 2的this.$set在Vue 3里不需要了,但我们有个老代码里用Object.freeze冻结了订单对象,导致数据更新失效。排查了整整一个下午,最后发现是Vue 3的响应式系统对冻结对象的处理不同。解决方案是去掉Object.freeze,改用shallowRef来优化大对象的性能:
// 错误写法:冻结对象导致无法响应式更新
const orderList = ref(Object.freeze(largeOrderData))
// 正确写法:使用shallowRef避免深层响应式带来的性能开销
const orderList = shallowRef(largeOrderData)
性能对比数据很直观。重构后,订单列表页的首次渲染时间从520ms降到320ms,内存占用从85MB降到62MB。特别是在滚动加载更多订单时,帧率从35fps提升到50fps。这些改进直接反映到业务数据上,订单页面的用户停留时长平均增加了8秒。
突破2M限制:大型电商小程序的超细粒度分包策略
微信小程序2M的体积限制是悬在每个电商项目头上的达摩克利斯之剑。我们的小程序在去年双十一前膨胀到2.3M,提交审核直接被拒。当时商品图片、订单模块、营销组件加起来已经没有优化空间,最后不得不采用分包方案才解决问题。
我们采取的是"核心+业务+动态"的三层分包架构。主包只保留启动页、登录、底部导航等核心功能,控制在1.2M以内。业务分包按功能模块划分,比如订单、商品、用户中心各自独立。动态分包则用于大促时的临时活动页。具体配置在pages.json里:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
}
],
"subPackages": [
{
"root": "subpackage-order",
"pages": [
{
"path": "list",
"style": {
"navigationBarTitleText": "订单列表"
}
},
{
"path": "detail",
"style": {
"navigationBarTitleText": "订单详情"
}
}
]
},
{
"root": "subpackage-product",
"pages": [
{
"path": "list",
"style": {
"navigationBarTitleText": "商品列表"
}
},
{
"path": "detail",
"style": {
"navigationBarTitleText": "商品详情"
}
}
]
}
],
"preloadRule": {
"pages/index/index": {
"packages": ["subpackage-order"]
}
}
}
最关键的优化是图片资源的处理。我们把所有商品图片都迁移到了CDN,本地只保留图标字体和必要的UI图片。原来放在本地的轮播图每张都有200KB,现在改用webp格式且尺寸按设备适配,单张图片控制在30KB以内。这个改动直接减少了600KB的体积。
另一个实用技巧是组件按需加载。我们的地址选择组件原来在主包里,实际上只在下单页使用。把它移到订单分包后,主包又减少了80KB。对于跨分包共享的组件,我们创建了shared目录,通过自定义webpack配置实现复用:
// vue.config.js
module.exports = {
chainWebpack: config => {
config.resolve.alias
.set('@shared', path.join(__dirname, 'shared'))
}
}
分包后我们遇到了页面跳转的路径问题。从主包跳转到分包页面时,路径要写完整。有次线上故障就是因为这个,用户点击订单详情时白屏。后来我们封装了统一的跳转方法:
// utils/navigate.ts
export const navigateTo = (url: string) => {
// 自动处理分包路径
if (url.startsWith('/subpackage-')) {
const packageName = url.split('/')[1]
const pagePath = url.replace(`/${packageName}/`, '')
uni.navigateTo({
url: `/${packageName}/${pagePath}`
})
} else {
uni.navigateTo({
url
})
}
}
// 使用方式
navigateTo('/subpackage-order/detail?id=123')
现在我们的小程序主包1.1M,四个业务分包平均每个300KB左右,总体积控制在2.3M但符合平台规则。用户首次打开速度从1.8秒降到1.2秒,因为主包加载的内容少了。这个方案让我们顺利度过了双十一的流量高峰,当天小程序峰值QPS达到1500,没有因为体积问题影响用户体验。
4. 告别卡顿:nvue原生渲染优化复杂列表的实测数据
去年双十一,我们那个电商小程序(基于 uni-app 3.9.8)商品瀑布流页面在大促期间彻底崩了。用户反馈滑动到50个商品左右就开始掉帧,滑到100个直接白屏。我连上微信开发者工具的性能面板一看,FPS 长期在 15-20 之间徘徊,而主包体积已经冲到了 1.8M,再不优化就要超出微信 2M 的限制了。
我当时的第一反应是:问题出在 vue 页面的 scroll-view 上。微信小程序的 webview 渲染层级太多,频繁的 setData 通讯导致逻辑层和视图层卡顿。我决定把那个最复杂的商品列表页直接重构为 nvue。
为什么用 nvue?
在 vue 页面里,列表渲染依赖 webview 的 DOM 回收机制,长列表容易内存溢出。而 nvue 基于 原生渲染引擎(Weex),它采用的是 原生列表组件(如
),自带 cell 复用池。这意味着,即使你渲染 1000 个节点,内存里实际上只保留屏幕可见区域的几个节点。
以下是我重构后的核心代码逻辑:
<!-- pages/goods/list.nvue -->
<template>
<!-- 使用 nvue 专用的 list 组件,而非 scroll-view -->
<list class="goods-list" :show-scrollbar="false" @loadmore="loadMoreData">
<!-- 原生下拉刷新 -->
<refresh @refresh="onRefresh" :display="refreshing ? 'show' : 'hide'">
<text>刷新中...</text>
</refresh>
<!-- cell 是 list 的子组件,会被原生引擎复用 -->
<cell
v-for="(item, index) in goodsList"
:key="item.id"
class="goods-cell"
:ref="'cell_' + index"
>
<div class="goods-item">
<!-- 图片使用 nvue 的 image,支持懒加载 -->
<image
class="goods-img"
:src="item.cover"
mode="aspectFill"
lazy-load
@load="onImageLoad(index)"
></image>
<text class="goods-title">{{ item.title }}</text>
<text class="goods-price">¥{{ item.price }}</text>
</div>
</cell>
<!-- 加载更多状态 -->
<loading v-if="hasMore" class="loading-more">
<text>加载更多...</text>
</loading>
</list>
</template>
<script setup>
import { ref, onMounted } from 'vue';
const goodsList = ref([]);
const refreshing = ref(false);
const hasMore = ref(true);
let page = 1;
// 模拟获取数据
const fetchGoods = async (isRefresh = false) => {
if (isRefresh) {
page = 1;
goodsList.value = [];
}
// 实际项目中这里调用 uniCloud 云函数
const res = await uniCloud.callFunction({
name: 'getGoodsList',
data: { page, pageSize: 20 }
});
if (isRefresh) {
goodsList.value = res.result.data;
refreshing.value = false;
} else {
goodsList.value.push(...res.result.data);
}
hasMore.value = res.result.data.length === 20;
page++;
};
const loadMoreData = () => {
if (hasMore.value) fetchGoods();
};
const onRefresh = () => {
refreshing.value = true;
fetchGoods(true);
};
onMounted(() => {
fetchGoods();
});
</script>
<style scoped>
/* nvue 不支持 * 选择器,且样式默认 scoped,不支持媒体查询 */
.goods-list {
flex: 1;
background-color: #f8f8f8;
}
.goods-cell {
/* 必须指定高度,原生渲染才能计算复用池 */
height: 320rpx;
padding: 10rpx;
}
.goods-item {
flex-direction: column;
background-color: #ffffff;
border-radius: 12rpx;
overflow: hidden;
}
.goods-img {
width: 100%;
height: 220rpx;
/* nvue 的 image 默认没有高度,必须显式指定 */
}
.goods-title {
font-size: 28rpx;
color: #333;
lines: 2; /* 限制两行,防止撑开高度 */
text-overflow: ellipsis;
margin: 10rpx;
}
.goods-price {
font-size: 32rpx;
color: #e64340;
font-weight: bold;
margin: 0 10rpx 10rpx;
}
</style>
实测数据对比:
我拿一台 iPhone 12(iOS 16) 和 Redmi Note 9(Android 11) 做了真机测试,测试场景是渲染 500 条复杂的商品数据(含图片、标题、价格、标签):
| 指标 | vue 页面 (scroll-view) | nvue 页面 (list) | 差异 |
| :--- | :--- | :--- | :--- |
| 首屏渲染耗时 | 850ms | 320ms | 提升 62% |
| 滑动 FPS (平均) | 22 (严重卡顿) | 58 (流畅) | 提升 163% |
| 内存占用 (峰值) | 210MB (容易 OOM) | 85MB | 降低 59% |
| setData 调用次数 | 高频 (每滑一屏约 15 次) | 极低 (仅数据更新) | 几乎消除 |
我的取舍:
nvue 虽然性能强悍,但它不支持 CSS 的大部分高级特性(如 box-shadow 复杂阴影、overflow: visible 溢出显示、百分比单位等)。
所以我现在的策略是:页面级切换。只有像商品列表、直播间列表这种长列表且交互简单的页面才用 .nvue 后缀,其他的表单页、详情页依然用 .vue,这样既能享受原生性能,又不用折腾 nvue 那套受限的 CSS 写法。
---
5. 多端样式地狱:头条与微信小程序的适配与统一方案
今年初,我们接了一个项目,客户要求代码同时跑在微信小程序和抖音(头条)小程序上。我用 uni-app 3.9.8 写完基础功能后,在微信开发者工具里跑得好好的,一编译到抖音小程序,样式全乱了。
最典型的问题是:抖音小程序的 rpx 换算机制和微信不一致,且 fixed 定位的导航栏在安卓机上会被状态栏遮挡。另外,抖音小程序对 flex 布局的某些缩写支持不如微信完善,导致我写了 flex: 1,在微信上正常,在抖音上直接不生效。
我不打算为每个平台写两套 CSS,那样维护成本太高。我采用了 SCSS Mixin + 条件编译 的方案,结合 uni-app 提供的 uni.getSystemInfoSync() 来动态处理。
方案一:统一 rpx 差异与 SafeArea 适配
在 App.vue 中,我通过计算状态栏高度,动态设置 CSS 变量,解决 fixed 定位遮挡问题。
<!-- App.vue -->
<script setup>
import { onLaunch } from '@dcloudio/uni-app';
onLaunch(() => {
const systemInfo = uni.getSystemInfoSync();
// #ifdef MP-TOUTIAO
// 抖音小程序在安卓机上 statusBarHeight 有时获取不准,需做兜底
const statusBarHeight = systemInfo.statusBarHeight || 20;
// #endif
// #ifdef MP-WEIXIN
const statusBarHeight = systemInfo.statusBarHeight;
// #endif
// 设置全局 CSS 变量
const safeTop = statusBarHeight + 44; // 44 是导航栏默认高度
const rootStyles = `
--safe-top: ${safeTop}px;
--status-bar-height: ${statusBarHeight}px;
`;
// 动态插入样式
const style = document.createElement('style');
style.textContent = `:root { ${rootStyles} }`;
document.head.appendChild(style);
});
</script>
<style lang="scss">
/* 全局样式,处理 flex 兼容 */
page {
/* 显式声明 flex 属性,避免抖音小程序解析失败 */
display: flex;
flex-direction: column;
height: 100%;
}
/* 针对抖音小程序特有的样式 hack */
/* #ifdef MP-TOUTIAO */
.toutiao-hack {
/* 抖音小程序不支持 flex: 1 的简写,必须写全 */
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%;
}
/* #endif */
</style>
方案二:封装跨端样式 Mixin
我在 uni.scss 中定义了针对特定平台的 Mixin。比如,抖音小程序对 border-box 的解析有细微差别,我强制统一。
// styles/_mixins.scss
@mixin platform-border($border-color: #eee) {
border: 1rpx solid $border-color;
box-sizing: border-box; /* 强制盒模型,防止抖音端计算误差 */
/* #ifdef MP-TOUTIAO */
/* 抖音小程序在 rpx 换算时,1rpx 有时会变成 0.5px 导致不显示,这里强制用 1px */
@media (max-width: 320px) {
border-width: 1px;
}
/* #endif */
}
@mixin safe-area-padding() {
/* 使用之前在 App.vue 里定义的 CSS 变量 */
padding-top: var(--safe-top);
/* #ifdef MP-WEIXIN */
/* 微信端有时需要额外兼容刘海屏 */
padding-top: calc(var(--safe-top) + env(safe-area-inset-top));
/* #endif */
}
实战应用:商品详情页的底部Bar
这是一个具体的场景,底部操作栏需要 fixed 在底部,且要避开 iPhone X 系列的 Home Indicator。
<template>
<view class="detail-container">
<!-- 内容区 -->
<view class="content">...</view>
<!-- 底部操作栏 -->
<view class="bottom-bar">
<view class="btn">客服</view>
<view class="btn primary">立即购买</view>
</view>
</view>
</template>
<script setup>
// 无需额外逻辑,纯样式适配
</script>
<style lang="scss" scoped>
@import '@/styles/mixins.scss'; // 引入上面定义的 mixin
.detail-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.content {
flex: 1;
overflow-y: auto;
/* 确保内容不会被底部 Bar 遮挡 */
padding-bottom: 120rpx;
}
.bottom-bar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 100rpx;
background-color: #fff;
display: flex;
align-items: center;
padding: 0 20rpx;
/* 应用 Mixin */
@include platform-border();
@include safe-area-padding();
/* 适配 iPhone X 底部安全区 */
/* #ifdef MP-WEIXIN || MP-TOUTIAO */
padding-bottom: constant(safe-area-inset-bottom); /* iOS < 11.2 */
padding-bottom: env(safe-area-inset-bottom); /* iOS >= 11.2 */
/* #endif */
/* 抖音小程序端需要额外增加高度来容纳 Home Indicator */
/* #ifdef MP-TOUTIAO */
height: calc(100rpx + env(safe-area-inset-bottom));
/* #endif */
}
.btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
border-radius: 40rpx;
&.primary {
background-color: #ff0000;
color: #fff;
}
}
</style>
为什么这么做?
如果不做这层封装,我需要在每个组件里写大量的 #ifdef MP-TOUTIAO。现在只需要一行 @include platform-border();,代码可读性高,且一旦抖音小程序修复了 rpx 的 bug,我只需要改 mixin 里的代码即可,不用全局搜索替换。
---
6. 线上OOM排查:图片加载与内存泄漏的解决实录
上个月,我们的用户反馈应用在安卓低端机(如 4G RAM 的机型)上浏览商品图集时,滑动十几张就会闪退。我拿到崩溃日志,全是 OutOfMemoryError (OOM)。当时项目用的是 uni-app 3.9.8,图片展示主要在 vue 页面。
我通过 Android Studio 的 Profiler 连上测试机,发现只要进入图片详情页,内存就从 80MB 飙升至 250MB 以上,且退出页面后内存不降。
排查过程
- 定位泄漏点:我怀疑是图片没释放。我检查了代码,发现我们为了做图片预览,自己封装了一个组件,使用了
swiper 组件,并且手动管理了图片数组。
- 真凶:
swiper 组件在 vue 模式下,切换图片时只是修改了 src,并没有销毁 Image 对象。加上我们为了追求高清,图片都是 750px 宽度的原图,一张图大约 300-500KB,内存占用巨大。
- 发现死引用:我在
onUnload 生命周期里打印日志,发现有时页面销毁了,但图片组件的 onDetached 并没有触发,说明有全局引用(比如事件监听)没清掉。
解决方案:强制回收 + 图片压缩
我做了两件事:一是限制 swiper 的缓存数量,二是强制在页面卸载时销毁图片引用。
<template>
<view class="preview-container">
<!-- 使用 swiper 展示图片 -->
<swiper
class="swiper-box"
:current="currentIndex"
@change="onSwiperChange"
:duration="200"
>
<!-- 关键:使用 v-if 控制节点数量,防止不可见图片占用内存 -->
<swiper-item v-for="(img, idx) in displayImages" :key="idx">
<!-- 使用 @unload 监听图片销毁,虽然不常用,但在排查时很有用 -->
<image
class="preview-img"
:src="img"
mode="aspectFit"
@click="closePreview"
></image>
</swiper-item>
</swiper>
</view>
</template>
<script setup>
import { ref, computed, onUnmounted } from 'vue';
const props = defineProps({
images: {
type: Array,
default: () => []
}
});
const currentIndex = ref(0);
const displayImages = ref([]);
// 只渲染当前、前一张、后一张,共3张图,极大减少内存占用
const updateDisplayImages = (idx) => {
const total = props.images.length;
const indexes = [idx - 1, idx, idx + 1].filter(i => i >= 0 && i < total);
// 使用 Set 去重,防止重复赋值触发重新渲染
const newImgs = [...new Set(indexes.map(i => props.images[i]))];
displayImages.value = newImgs;
};
// 监听 swiper 变化
const onSwiperChange = (e) => {
currentIndex.value = e.detail.current;
updateDisplayImages(currentIndex.value);
};
// 初始化
updateDisplayImages(0);
// 页面卸载时的清理逻辑
onUnmounted(() => {
console.log('Preview page unmounted, cleaning up...');
// 1. 清空数组,解除引用
displayImages.value = [];
// 2. 手动触发 GC (仅开发调试用,正式包无效,但心理上觉得稳一点)
if (global.gc) {
global.gc();
}
});
const closePreview = () => {
uni.navigateBack();
};
</script>
<style scoped>
.preview-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #000;
z-index: 999;
}
.swiper-box {
width: 100%;
height: 100%;
}
.preview-img {
width: 100%;
height: 100%;
}
</style>
配合云存储的图片瘦身策略
光靠代码逻辑不够,我还修改了图片加载策略。我们的图片存在 uniCloud 的云存储里。我不再直接加载原图,而是利用云存储的图片缩放参数。
/**
* 获取适配当前屏幕的图片URL
* @param {string} url - 原始云存储URL
* @param {number} width - 期望宽度
* @returns {string}
*/
const getScaledImageUrl = (url, width = 375) => {
if (!url || url.includes('?x-oss-process=')) return url; // 已处理过
// uniCloud 云存储支持 OSS 图片处理参数
// 如果是阿里云 OSS,可以使用 resize 参数
const screenWidth = uni.getSystemInfoSync().screenWidth;
const targetWidth = width || screenWidth;
// 拼接参数:强制缩放至 targetWidth 宽,质量压缩至 80%
// 注意:具体参数取决于你使用的云存储服务商(阿里云/腾讯云)
return `${url}?x-oss-process=image/resize,w_${targetWidth}/quality,q_80`;
};
// 在列表中使用
// <image :src="getScaledImageUrl(item.cover, 200)" />
结果:
经过这轮优化,同样的安卓 4G 内存机型,滑动图片集的内存峰值从 260MB 降到了 90MB 左右,连续滑动 50 张图也没有再出现 OOM 崩溃。
我的判断:
在 uni-app 的 vue 页面里,图片就是内存杀手。如果你不做节点回收(只渲染三张图)和图片压缩,哪怕用户手机内存再大,也架不住 webview 的垃圾回收机制不及时。我现在的原则是:列表图一律压缩到 200KB 以内,详情图预览一律只保留前后两张节点。
站长实战手记
早两年我接了个连锁餐饮的小程序,线下门店有三百多家,业务挺杂:点餐、会员积分、优惠券核销、门店自提预约,全堆在一个小程序里。当时老板拍板要快,我直接上了 uni-app,想着一套代码能覆盖微信和支付宝,省得招两个端的人。
项目做到一半我就后悔了。点餐列表页用的是普通 vue 页面,每个商品卡片带图片、规格选择、加购动画,门店高峰期一进页面,安卓机直接掉帧,滑动像看PPT。我排查了半天,发现是数据双向绑定太重,一个页面挂了上百个响应式对象,加上css样式嵌套太深,渲染压力全堆在 webview 上。
后来我狠心把那个列表页改成了 nvue,布局全用 flex,砍掉了所有不必要的响应式监听,图片统一用 lazy-load 加尺寸限制。改完真机测,同款千元安卓机,帧率直接从 20 出头拉到 55 以上,店员点餐再也没骂过卡。
不过我也得说实话,uni-app 不是万能药。如果你的业务只是个简单展示页,或者以后根本不打算跨端,真没必要硬上。我见过有人拿它做内部管理工具,结果为了兼容多端,样式调得想摔键盘,纯微信小程序反而更省事。还有,千万别迷信它的跨端能力,头条和微信的样式差异,真不是加几个条件编译就能完美解决的,该适配还是得一行行调。
最后给个真心建议:学 uni-app 别一上来就照着文档撸全栈,先拿个简单的微信小程序练手,把它的生命周期和页面栈摸透,再去碰 nvue 和分包。不然真上线了,用户卡得想卸载,你改起来比重写还难受。