在 Flutter 社区里,“性能”这个词往往被分成两派:一派盯着 FPS、帧耗时、内存占用这些冷冰冰的数字,另一派关注架构分层、状态管理、依赖注入这些抽象概念。可真正在大型项目里摸爬滚打过的开发者都清楚,性能设计从来不是某一次优化能解决的。它是一套从架构通信方式、渲染管线选型、原生桥接策略到构建打包脚本的全链路设计,任何一个环节埋了雷,等业务模块铺开之后再去拆,成本高到你怀疑人生。
这篇指南是我在一个拥有几十个业务模块、跨 Android / iOS / 鸿蒙多端发布的大型 Flutter 项目中沉淀下来的实战总结。适合已经开始用 Flutter 做正经产品、正在为线上卡顿、启动耗时、页面状态丢失以及原生交互性能头痛的团队参考。里面不会只讲“用 Profile 模式别用 Debug 模式”这种正确的废话,而是从组件通信怎么选、Navigator 切换页面状态怎么保、Impeller 引擎怎么开、PlatformView 怎么嵌、EventChannel 和 MethodChannel 怎么划边界,再到 Gradle 打包报错这种边缘问题,一整套可以直接照抄的设计思路和踩坑记录。
1. 大型项目性能设计的第一关:架构与通信方式
很多人一聊 Flutter 性能,上来就盯 Widget build 方法的效率,这当然没错,但大型项目真正的性能隐患往往埋在更上游的架构层。模块之间怎么通信、数据由谁持有、页面依赖什么样的数据源,这些决策会在业务增长后被无限放大。通信层级乱了,一个状态变更触发几十个 Widget 重建,帧耗时自然爆炸。
1.1 组件通信方案怎么选才不拖累性能
组件通信是大型项目最容易被低估的性能陷阱。小项目里父子传参、回调搞定一切,但大项目动辄几十个页面、十几个核心模块,如果通信方案一开始没定调,后续改起来真的是抽丝剥茧。
常见的通信方式有这么几种,我分别说说它们在大型项目里的表现。
回调(Callback)是最基础、也最容易被滥用的方式。父子组件之间低频事件用回调没问题,但回调层级一深,代码可读性急剧下降,而且每次回调触发时,如果你的父组件没有做局部刷新隔离,很容易导致整棵子树重建。我在项目里见过一个三层嵌套组件,内层一个滑块拖动,回调被逐层冒泡,最外层 setState 重建了整个页面,流畅度直接被打到 20 帧。
InheritedWidget 是 Flutter 系统级的共享机制,性能好、零额外依赖,适合主题、语言、登录态这类全局基础数据。但它有个明显的坑:依赖关系是“就近继承”,业务组件想跨层拿数据必须靠 context 一路找,代码里写出来比较绕。而且大型项目里如果滥用 InheritedWidget,会导致依赖它的组件在每次变更时全部重建,控制不好粒度一样卡。
Provider 和 Riverpod 是目前社区的主流方案。我个人的建议是,新项目直接考虑 Riverpod,它的自动失效机制和细粒度监听对大型项目太友好了。Riverpod 的 Provider 可以做到数据变化时只重建真正监听该数据的组件,而不是整个页面。相比之下,Provider 的 ChangeNotifier 在 notifyListeners 时是广播式的,一个通知出去,所有监听对象都会回调,很容易在不知不觉中放大重建范围。
Bloc 是另一个主流选择,事件驱动、流式处理,业务逻辑非常清晰,适合中大型团队做规范化开发。但 Bloc 也有性能代价:Stream 的创建、订阅、取消都需要开发者主动管理,稍不注意就会内存泄漏,而且 Stream 事件在事件循环里的调度频率如果过高,会挤压 UI 帧生产的时间片。
我的实践经验是:大型项目不要迷信单一通信方案,要混合用。页面内部的局部状态用 StatefulWidget + 回调解决;跨业务模块的共享数据用 Riverpod 或 Bloc;全局基础设施(登录态、租户信息、主题配置)用轻量级的 InheritedWidget 或 Riverpod 的 override 机制。关键是确保“通知粒度”尽可能小,数据的变更只影响真正需要它的那部分 UI。
1.2 系统架构分层:性能得在设计阶段定调
聊完通信,再往上一层,是整个 App 的系统架构。这件事听起来和性能无关,但实际关系极大。Flutter 自身的系统架构分三层:Framework 层是用 Dart 写的组件、渲染、手势、动画体系;Engine 层负责 Skia / Impeller 渲染、Dart 运行时、文本布局;Embedder 层则是各平台的壳工程,负责把 Engine 和原生系统桥接起来。
理解这三层结构对性能设计非常重要。比如你发现动画卡顿,要先判断卡顿发生在 Framework 层的 build/layout 阶段,还是 Engine 层的 raster 阶段。如果是 build 阶段耗时,那是 Dart 侧的组件重建问题;如果是 raster 阶段耗时,比如过度绘制、复杂 shader,那就要从绘制策略上想办法,比如减少透明层叠、使用 RepaintBoundary。
应用层的分层策略同样影响性能。我见过不少项目把所有网络请求、数据库读写、页面状态全部塞进 Widget 的 initState 里,看起来代码不多,可一旦页面变得复杂,build 方法里全是异步依赖和全局 Store 访问,任何一个数据的更新都会牵动整页刷新。正确做法是把数据层、业务层和 UI 层解耦,UI 只消费经过业务层处理后的最小数据集合,不要让 Widget 直接触发重型计算。
拿 Flutter 和其他前端框架横向对比一下,也能看出分层对性能的意义。React Native 走的是 JS 桥通信,UI 更新需要跨语言桥接,高频交互下的开销很明显;Flutter 的 Dart 代码直接由 Engine 执行,UI 构建和布局绝大部分不跨进程,性能更稳定。但 Flutter 一旦需要原生能力,就必须走 MethodChannel / EventChannel,这个跨桥成本是真实存在的。因此我在大型项目里有一条硬性约定:“凡是能放 Flutter 侧实现的能力,坚决不往原生丢;凡是必须走原生桥的调用,必须做频率控制和数据压缩。”
2. 渲染管线与 UI 流畅度:从 Impeller 到高频交互细节
架构层把树理顺之后,真正让用户感知到“流畅”的,还得看渲染管线。Flutter 的每一帧都要经过 build、layout、paint、composite 几个阶段,任何一个阶段超时都会产生掉帧。大型项目里常见的卡顿源无非是列表快速滚动、下拉刷新、页面转场动画这几类高频场景,咱们逐个拆。
2.1 Impeller 渲染引擎:为什么要重点关注它
Impeller 是 Flutter 新一代渲染引擎,近两个大版本在 iOS 上默认启用,Android 端也逐步开放。它要解决的核心问题是 Skia 时代的“着色器编译卡顿”(Shader Compilation Jank)。
解释一下背后的原理:Skia 渲染时,GPU 上要运行很多自定义着色器,这些着色器通常是在首次绘制到该区域时才实时编译。同一个页面,你第一次滑动很流畅,第二次滑动却卡一下,往往就是这个编译过程在作怪。Impeller 的做法是提前把着色器预编译成 Metal / OpenGL 对应的中间格式,运行时不再等编译,从源头消掉这个不可预测的顿卡。这对大项目的实际感知提升非常明显,尤其是页面元素复杂、动效繁多的场景,启动和首滑的稳定性好很多。
实操上,只要你的 Flutter 版本在 3.10 以上,在 Android 工程里可以通过flutter run --enable-impeller验证效果。iOS 上新版默认就是 Impeller,不需要额外操作。需要提醒的是,Impeller 初期对一些自定义着色器(Custom Shader)和部分 PlatformView 的兼容性仍有边际问题,我曾在某个需要高频嵌入相机预览的页面上遇到过纹理异常,后来临时把该页面切回 Skia 渲染才解决。所以引入 Impeller 之前,建议把项目里的高端自定义绘制场景都过一遍真机回归。
2.2 高频 UI 场景:下拉刷新、列表滚动的实战优化
下拉刷新是大型 App 里最容易被写崩的组件。很多人直接在RefreshIndicator.onRefresh里放一个 Future,这个 Future 体量如果很重,比如同步做了本地数据库查询、多个网络请求、接着 setState 全页刷新,那下拉回弹动画就会明显卡顿。正确做法是 onRefresh 只管拉数据,让回调里返回的 Future 保持轻量;数据回来之后,用 ValueNotifier 或者局部的状态管理去更新列表数据源,避免整页 setState。
列表是另一个重灾区。ListView.builder是基础要求,但我见过不少项目为了省事,直接把一个长列表塞进Column+SingleChildScrollView,数据量一大就全员构建,卡到没法看。除了用 builder,还需要注意 item 自身的构建成本:item 里不要写复杂的联动逻辑,图片要加缓存和占位,不要在 item 的 build 方法里做任何集合遍历或字符串拼接之外的重活。
再往下走,有几个容易被忽视的重绘细节。第一是 const 构造器,能用 const 的地方一定要用,这能帮 Flutter 跳过很多不必要的组件重建;第二是拆分 Widget,一个巨型 build 方法里塞二三十个组件,哪怕只改一个局部变量也会整体重建,拆成多个小组件后,RepaintBoundary可以帮你把重绘锁定在小范围内;第三是高频动画里的图片,尽量用RepaintBoundary包一层,减少 raster 阶段的画布提交面积。
2.3 PlatformView 嵌入原生视图:性能与兼容的平衡
大型项目里完全绕开 PlatformView 不太现实,地图、相机预览、部分视频播放器都依赖原生视图。Flutter 提供 PlatformView 机制把这些原生视图嵌入 Flutter 的合成树里,但代价是渲染路径会变得特殊。
PlatformView 有两种实现模式:早期的 Virtual Display 和后来的 Hybrid Composition。Virtual Display 把原生视图渲染到一个虚拟显示里,再通过纹理送给 Flutter,兼容性尚可,但手势和输入事件处理有延迟感;Hybrid Composition 让原生视图和 Flutter 视图在同一个合成树里直接叠加,实时性好很多,但也牺牲了 Flutter 那边的一些绘制优化空间。
实战中有几条硬经验:尽量不要在滚动的列表里大量使用 PlatformView。列表滚动时 Flutter 侧和原生侧的合成需要持续同步,PlatformView 一多,滑动掉帧几乎是必然的。如果只是展示静态地图截图或视频缩略图,优先用 Flutter 侧的自绘组件代替;非要嵌入,也要确保 PlatformView 的数量可控,并且不要让它在屏幕外仍保持活跃。另外,Hybrid Composition 模式下原生 SurfaceView / TextureView 的层级处理一直很麻烦,遇到闪烁或者黑屏,先试试调整 AndroidManifest 里的hardwareAccelerated配置,再查一下原生侧的 surface 回调时机。
3. 原生能力桥接:MethodChannel、EventChannel 与系统级功能
纯 Flutter 侧的优化做完,大型项目还得面对一个无法回避的部分:原生能力集成。业务里总有用 Dart 搞不定的场景,比如读取设备唯一标识、接入系统级的 Live Activity、跳转某个已有的原生 Activity。桥接方案选得好,不仅能满足功能,还能把性能损耗控制在可接受范围。
3.1 MethodChannel 与 EventChannel:各自的分工与边界
Flutter 和原生通信有两个最常用的通道:MethodChannel 适合“请求-应答”模式,比如 Flutter 调一个方法,原生处理完返回结果;EventChannel 适合“持续推送”模式,比如传感器数据、位置更新、原生侧主动发起的事件流。
MethodChannel 的典型用法是低频、一次性调用。比如读取手机型号、获取电池电量、唤起一个原生相册:
// Dart 侧 final channel = const MethodChannel('com.example.device'); final int? batteryLevel = await channel.invokeMethod<int>('getBatteryLevel');EventChannel 则适合高频或订阅式数据。比如监听原生侧的音量按键、传感器原始数据:
// Dart 侧 const eventChannel = EventChannel('com.example.sensor'); eventChannel.receiveBroadcastStream().listen((event) { // 接收高频连续事件 handleSensorEvent(event); });性能方面有几条我在项目里严格执行的规范。第一,高频数据不要用 MethodChannel 频繁双向调用。每走一次通道都涉及编码、跨层拷贝、线程切换,频率一高很容易挤压 UI 线程的事件处理时间。比如实时位置上报,原生侧应该按时间窗口或距离阈值合并数据,而不是每 100 毫秒就往 Flutter 丢一条。第二,跨桥传递的数据要做减法,能传基础类型就传基础类型,别硬塞大 Map 或 Base64 图片。大数据对象的序列化成本在大型项目里会被指数级放大,真有图片和文件需求,优先传文件路径,让 Flutter 侧自己读取。第三,EventChannel 记得要处理取消订阅,页面销毁时cancel()一定要调用,否则原生侧还在持续回调,轻则内存泄漏,重则事件堆积导致 Dart 事件循环被堵死。
3.2 原生 Activity 跳转、LiveActivity 与跨端适配
跨端集成里最常遇到的需求是 Flutter 跳转原生 Activity。常规做法是在 MethodChannel 里定义一个跳转方法,原生侧接收后用 Intent 启动目标 Activity,等原生 Activity 结束返回时,再通过另一个 MethodChannel 回调或者 Activity Result 把数据带回 Flutter。
原生侧的大致逻辑(Kotlin):
class MainActivity : FlutterActivity() { private val channelName = "com.example.navigation" override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName) .setMethodCallHandler { call, result -> if (call.method == "openNativeActivity") { val intent = Intent(this, TargetNativeActivity::class.java) startActivity(intent) result.success("opened") } else { result.notImplemented() } } } }这个方案的性能关键在于尽量减少通道调用次数和参数体积。跳一次原生页面只调一次通道,没什么压力,但如果你在跳转参数里塞一个超大的序列化对象,那原生 Activity 的启动时间、IPC 开销都会明显增加。我通常的做法是只传必要 ID,由原生侧自己根据 ID 加载数据。
再来说 Live Activity。这个是 iOS 端灵动岛和锁屏实况组件的系统级能力,Flutter 本身没有直接的 API,必须通过原生侧实现。原生侧用 ActivityKit 创建和更新 Live Activity,Flutter 侧再通过 MethodChannel 把动态状态(比如外卖配送进度、运动数据)同步给原生侧,由原生侧负责刷新实时区域。这里最容易踩的坑是频繁更新——Live Activity 的实时刷新有系统频率限制,业务方如果拿着秒级数据反复推送,除了耗电,还会被系统拒掉或降级。我们项目里直接把推送逻辑做了节流,只在状态真正变更时调用更新接口。
跨端适配也是大型项目绕不开的硬骨头,尤其当你的目标端不止 Android 和 iOS,还有鸿蒙的时候。Flutter 在鸿蒙上跑的是 OpenHarmony 的桥接环境,原来的 Android 原生插件都需要移植成鸿蒙插件。我们做适配时的原则是:抽象出一个平台适配层,Flutter 侧的业务代码只面向接口编程,平台侧各自实现 channel handler。这样既避免了业务层因平台差异产生分叉,也方便后续维护不同端的性能参数。最怕的就是在 Flutter 业务代码里到处写if (Platform.isAndroid)这类分支,性能排查和逻辑维护都会变成灾难。
4. 导航、异步与状态恢复:隐藏的性能暗礁
导航和异步是 Flutter 面试、日常开发都会高频遇到的主题,但很多人只停留在“会调用”的层面,不清楚背后的运行时机制。其实 Navigator 切换页面的状态保存、Future 的微任务调度,都会直接影响大型 App 的稳定性和流畅度。
4.1 Navigator 切换页面后状态会丢失吗?真相与对策
这个问题在 Flutter 社区里被反复翻出来讨论。先说结论:Navigator push 新页面之后,旧页面的 State 通常不会立即销毁,它会被 Route 以 Offstage 的方式保留在 Widget 树里。但“保留”不代表“永不丢失”。
当内存压力上升,或者系统回收了被压栈的页面所在的 Activity / ViewController 后,Flutter 框架有能力回收不可见的子树。页面再次回到前台时,State 可能是全新创建的一个,之前的滚动位置、表单内容如果没做保存,就全部归零了。这就是为什么大项目里经常出现“切到后台再回来,后续页面的滚动位置丢了”的现象。
对策主要有三招。第一,给可滚动组件设置PageStorageKey,让 Flutter 框架自动保留滚动偏移;第二,如果你希望某个列表页在压栈后仍然保持活跃状态,可以混合使用AutomaticKeepAliveClientMixin,但这会占用内存,用得越多,App 内存基线越高,批量使用前要想清楚代价;第三,也是我在大型项目里最推荐的做法:不要把页面依赖的关键数据只存在 State 里,核心业务数据放到 Store 层或持久化层,页面只是这些数据的视图。这样即使页面 State 被回收,重新构建时也能从数据源恢复,不依赖 Widget 树的存活。
另外要留意一个性能细节:PageStorageKey不是万能的。它保存的滚动位置是放在PageStorage桶里的,如果同一页面里多个可滚动组件用了重复的 key,或者数据量极大,存储在桶里的内容也会占用内存。大型项目里建议每个页面用唯一的 key 命名规范,避免冲突。
4.2 Future 的 then 回调:是微任务吗?写错就能拖垮 UI
很多刚从其他语言转过来的开发者会在面试题里看到这个问题:Flutter 里 Future 的 then 回调是不是放进微任务队列?答案是:Future.then 注册的回调会被调度到微任务队列(Microtask Queue)里执行,微任务在整个事件循环中的优先级高于事件队列(Event Queue)。
这意味着什么?Dart 的单线程事件循环里,每一帧的生产和 UI 事件处理都在事件队列里流转。你注册的大量微任务会在每个事件循环同步阶段被一并执行,如果微任务堆积得太多,就会挤压 UI 帧的生产时间,表现为掉帧、卡顿、交互延迟。
典型性能隐患有这么几类。一是在 build 方法里大量使用 FutureBuilder,每次重建都会创建新的 Future,产生大量微任务调度;二是用 Future.delayed 或周期性 Timer 模拟轮询,在列表页里尤其要避免;三是在一个同步链上连续await多个异步方法,实际上每个 await 都往微任务队列里塞回调,链条越长,当前事件循环被拖得越久。
优化经验是:高 CPU 消耗的任务不要放在主 Isolate 的微任务队列里,用compute或独立 Isolate 去跑;相对独立的数据源,用 Stream 实现真正的异步通知,而不是靠 Timer 反复轮询;对于实时性要求不高的场景,主动用scheduleMicrotask合并多个小更新,减少不必要的帧内调度次数。
5. 构建与打包阶段的性能陷阱
性能设计如果只盯着运行时,那你只解决了一半问题。大型 Flutter 项目的构建速度、打包稳定性同样是开发效率的生死线。构建脚本拖沓、打包频繁报错,会直接影响团队的迭代节奏。这一节聊几个我真实踩过的坑。
5.1 Gradle 配置方式与构建性能:不要再“命令式 apply”
你在搜索 Flutter 问题的时候,大概率刷到过这样一条报错:You are applying Flutter's main Gradle plugin imperatively using the apply script。这条信息来自新版 Flutter Gradle 插件的迁移提醒,说的是工程还在用老式的根级apply plugin方式引入 Flutter 插件,而新版建议改用 settings.gradle 里的插件声明式管理。
老式写法的坑不只是风格问题,它会在配置阶段把插件和依赖用脚本闭包方式逐个加载,构建时容易导致配置阶段耗时、依赖解析顺序混乱,尤其是大型项目多模块依赖时,配置阶段动不动就多出几十秒。改成声明式之后,Gradle 能更早地确认插件版本和仓库,可以利用配置缓存加速后续构建。
建议的迁移方式是,在settings.gradle里用pluginManagement管理 Flutter 相关插件的版本,然后在模块的build.gradle里用plugins { id "com.android.application" version "..." }声明。这个改动需要连同 AGP 版本一起评估,不要单独为了消警告而大改构建配置,否则容易引发其他依赖兼容问题。改完建议执行一次flutter clean,让 Gradle 重新解析依赖。
5.2 打包报错与 Dart VM 初始化异常的排查思路
大型项目打包报错的花样非常多,但高频的几个错误往往可以归纳为固定套路。
java.lang.AssertionError: java.lang.Exception: could not close ...这类错误,最常见的原因是构建过程中有文件句柄被占用,或者资源文件过大导致输出流无法正常关闭。排查顺序:先清理临时目录和 Gradle 缓存,执行flutter clean、删除.gradle目录;如果还报同一个错误,检查工程里是不是有超大图片或视频文件被打进了 assets,尝试降低资源体积;最后看看系统是不是有安全软件在锁定文件,Windows 上尤其容易遇到这类问题。
另一类高频问题是e/flutter (31173): [ERROR:flutter/runtime/dart_vm_initializer.cc(41)] unhandled exception之类以 flutter 开头的 Dart VM 初始化错误。这通常代表 main() 执行早期就发生了未捕获异常。常见诱因是:某个插件在注册阶段访问了原生侧特有的能力但没做兼容判断,或者初始化时依赖的全局配置还没有准备好。排查时先看异常完整堆栈,定位到具体调用的插件或初始化代码;再检查main()里的启动顺序,确保所有依赖的异步初始化都完成了再调用 runApp。
这类问题还要注意跨平台差异,同一个初始化流程在 Android 上没问题,在鸿蒙端可能会因为底层接口缺失而崩溃。我们的做法是给初始化逻辑包一层 try-catch,并加上平台能力探测,能力不具备就降级执行,保证 App 至少能启动而不是直接闪退。
6. 大型项目性能设计的落地经验:从基线到团队协作
性能设计如果只停留在代码技巧层面,很难真正落地。大型项目需要的是一套可量化、可检查、可持续迭代的机制。这一节聊聊我在团队里是怎么建立的性能基线、诊断工具和面试招人时的考察点。
6.1 建立性能预算:把性能变成“硬指标”
无预算不优化。性能设计的第一步,是把关键指标变成团队都能看到、都能负责的数字。我这里分享一个自己团队在用的基线参考:
| 指标 | 大型项目建议基线 | 说明 |
|---|---|---|
| 冷启动时间 | 3 秒以内(中端机) | 从点击图标到首页可交互 |
| 核心页面列表滑动 | 60 FPS,且帧耗时稳定在 12ms 以下 | 使用 Profile 模式验证 |
| 方法通道调用频次 | 单页面活跃场景低于 20 次/秒 | 高频场景必须合并或走 EventChannel |
| PlatformView 数量 | 单屏同时挂载不超过 2 个 | 超过则优先改用 Flutter 自绘 |
| 构建时间 | 增量构建不超过 3 分钟 | 依赖 Gradle 配置缓存和模块化 |
这些数字不是拍脑袋定的,而是根据大量真机测试得到的经验值。项目早期就把性能预算写在团队的开发规范里,新功能提测前先对照基线做一轮自测,比事后优化高效得多。
6.2 性能诊断工具与常见分析路径
工具用得好,性能问题的定位速度能快一个量级。Flutter 官方提供的 Profile 模式是前提,Debug 模式下 Dart 运行时会开启大量断言和检查,帧耗时参考意义不大,真正做性能分析必须用 Profile 或 Release 模式。
常用的诊断路径有三条。第一条是 DevTools 里的 Timeline 页,观察每一帧 build、layout、paint 三个阶段的耗时分布,哪个阶段长时间超过 16ms,问题大概率就出在哪,然后对症下药。第二条是 Memory 页,大型项目里内存泄漏通常表现为 DevTools 中 Dart Heap 持续上升,配合flutter memory命令可以定位泄漏对象。第三条是 Computer 中的 Raster 线程指标,如果 UI 线程正常但 Raster 线程满载,说明问题出在绘制和合成环节,考虑 RepaintBoundary 和过度绘制优化。
我习惯在每个重要版本发版前做一轮真机性能走查,重点覆盖首页首屏、列表页连续滚动 5 分钟、页面快速切换、拍照组件开启关闭、弱网环境下拉刷新这几类场景。在这个走查表里,基本能覆盖大部分线上性能问题的诱因。
6.3 面试高频点与团队技能沉淀
带团队做大型 Flutter 项目,招人的时候免不了要面 Flutter 性能设计能力。这里面的一个核心判断标准,是看对方能不能把原理和实践结合起来讲透。比如问 StatefulWidget 在什么情况下会重建、const 优化到底优化了什么、Future 的 then 回调与微任务队列的工作机制、PlatformView 为什么可能掉帧、Impeller 实际解决了什么问题。这些问题如果能从一个完整工程的视角来回答,说明对方是真的有大型项目实战经验,而不是只刷了面试题。
对团队内部,我建议把沉淀下来的性能案例写成 FAQ 文档,每次排查到新的坑就补充进去。项目迭代速度越快,这套知识库的价值就越大。
结尾一个小体会
说实话,Flutter 大型项目的性能设计没有银弹,每个方案都有取舍。我在接手这个项目最痛苦的一段时间,天天在列表滚动卡顿和原生通信延迟之间来回横跳,后来才慢慢意识到:性能问题的根子往往不在某一行代码,而在架构选型时的某个决定。组件通信粒度选小了,后续所有页面都会受益;PlatformView 一开始就控制数量,后面就不用反复重构。
所以我的最后一个建议是:大型项目的性能设计一定要从第一天开始做。你可以不用把每件事都做完美,但至少要把性能预算、通信规范、渲染策略这几件事定下来,让后续所有业务开发都往这个框架里放。等业务量真正上来的时候,你才会庆幸当初多花了那一周时间来定基线。