大厂一般不是“页面自己决定怎么加载”,而是做成一套:
配置化页面系统 + 组件注册系统 + 任务调度系统 + 缓存降级系统 + 埋点监控系统首页大致是这样:
App启动
↓
预拉取基础配置
↓
进入首页
↓
读取本地首页配置缓存
↓
快速渲染骨架/旧数据
↓
请求最新首页布局配置
↓
生成组件加载任务队列
↓
首屏组件优先加载
↓
非首屏组件按优先级/距离/空闲状态预加载
↓
组件独立成功/失败/重试
↓
曝光/点击/转化埋点一、大厂首页不会这样写
不会是:
HomePage initState() {
loadBanner();
loadMenu();
loadGoods();
loadCoupon();
}也不会是:
组件曝光了才开始请求因为这会导致:
首屏慢
滑到哪里卡到哪里
请求失控
没有统一优先级
无法灰度
无法配置
无法降级二、大厂更像这样
1. 首页布局接口
先请求一个“页面结构配置”。
{
"pageId": "home_main",
"version": "2026.04.30.001",
"style": {
"backgroundColor": "#F7F7F7",
"navigationBar": "transparent"
},
"components": [
{
"id": "banner_001",
"type": "banner",
"sort": 1,
"loadPolicy": {
"mode": "immediate",
"priority": 100,
"cache": "staleWhileRevalidate"
}
},
{
"id": "menus_001",
"type": "menus",
"sort": 2,
"loadPolicy": {
"mode": "firstScreen",
"priority": 90
}
},
{
"id": "recommend_001",
"type": "goodsList",
"sort": 10,
"loadPolicy": {
"mode": "preload",
"priority": 40,
"preloadDistance": 1600
}
}
]
}这个接口只负责告诉客户端:
有什么组件
什么顺序
什么样式
怎么加载
数据源在哪里
失败怎么降级
缓存怎么用三、客户端核心模块
大厂首页一般会拆成这些东西:
HomeLayoutManager
HomeComponentRegistry
HomeLoadPlanner
HomeLoadScheduler
HomeComponentLoader
HomeCacheManager
HomeExposureTracker分别负责:
HomeLayoutManager 首页配置
HomeComponentRegistry 组件注册
HomeLoadPlanner 生成加载计划
HomeLoadScheduler 调度加载任务
HomeComponentLoader 真实加载组件数据
HomeCacheManager 缓存与降级
HomeExposureTracker 曝光与点击埋点四、真正的加载逻辑
不是组件自己随便请求,而是生成一个任务队列:
[
banner_001 priority 100 immediate
menus_001 priority 90 firstScreen
coupon_001 priority 80 firstScreen
flash_sale_001 priority 70 afterFirstScreen
recommend_001 priority 40 preload 1600px
brand_001 priority 20 preload 800px
]然后调度器执行:
1. immediate 立刻执行
2. firstScreen 首屏并发执行
3. afterFirstScreen 首屏完成后执行
4. preload 根据滚动距离提前执行
5. lazy 曝光兜底执行
6. idle 在用户停止滑动/空闲时执行五、核心伪代码
class HomeLoadScheduler {
HomeLoadScheduler({
required this.loader,
required this.maxConcurrent,
});
final HomeComponentLoader loader;
final int maxConcurrent;
final Set<String> _loaded = {};
final Set<String> _loading = {};
Future<void> runInitial(List<ComponentTask> tasks) async {
final initialTasks = tasks.where((task) {
return task.mode == LoadMode.immediate ||
task.mode == LoadMode.firstScreen;
}).toList();
initialTasks.sort((a, b) => b.priority.compareTo(a.priority));
await _runWithLimit(initialTasks);
}
void onScroll({
required double scrollOffset,
required double viewportHeight,
required List<ComponentTask> tasks,
}) {
final preloadTasks = tasks.where((task) {
if (_loaded.contains(task.componentId)) return false;
if (_loading.contains(task.componentId)) return false;
final distance = task.topOffset - scrollOffset - viewportHeight;
return distance <= task.preloadDistance;
}).toList();
preloadTasks.sort((a, b) => b.priority.compareTo(a.priority));
_runWithLimit(preloadTasks);
}
Future<void> _runWithLimit(List<ComponentTask> tasks) async {
for (final task in tasks.take(maxConcurrent)) {
_load(task);
}
}
Future<void> _load(ComponentTask task) async {
_loading.add(task.componentId);
try {
await loader.load(task.componentId);
_loaded.add(task.componentId);
} finally {
_loading.remove(task.componentId);
}
}
}六、组件本身只负责展示
组件不负责决定什么时候请求。
class HomeComponentRenderer extends StatelessWidget {
const HomeComponentRenderer({
super.key,
required this.component,
required this.state,
});
final HomeComponent component;
final ComponentState state;
@override
Widget build(BuildContext context) {
return switch (state.status) {
ComponentStatus.loading => component.skeleton,
ComponentStatus.success => component.view,
ComponentStatus.failed => component.errorView,
ComponentStatus.empty => component.emptyView,
};
}
}七、组件注册系统
首页不应该写死:
if (type == 'banner') return BannerView();
if (type == 'menus') return MenusView();而是:
final registry = HomeComponentRegistry()
..register('banner', BannerComponentBuilder())
..register('menus', MenusComponentBuilder())
..register('goodsList', GoodsListComponentBuilder())
..register('coupon', CouponComponentBuilder());渲染时:
registry.build(component, state);这样以后加一个组件:
直播组件
秒杀组件
新人专享
排行榜
品牌专区只需要新增 Builder,然后注册进去。
八、缓存策略
大厂首页一般会有这些策略:
cacheFirst 先缓存,没缓存再请求
networkFirst 先网络,失败用缓存
staleWhileRevalidate 先显示旧缓存,同时后台刷新
cacheOnly 只用缓存
networkOnly 只走网络首页最常用的是:
staleWhileRevalidate也就是:
先显示上一次首页
同时请求最新配置和最新数据
请求成功后局部刷新这样首页不会白屏。
九、降级策略
组件失败不应该导致首页失败。
Banner 失败:显示默认 Banner 或隐藏
Menus 失败:显示缓存菜单
GoodsList 失败:显示重试按钮
Coupon 失败:直接隐藏
FlashSale 失败:隐藏整个模块配置里可以写:
"fallback": {
"type": "hide",
"useCache": true,
"retry": 2
}十、最终大厂形态
首页不是一个页面
首页是一个页面容器
组件不是写死 UI
组件是可注册插件
请求不是页面发起
请求是任务调度器发起
曝光不是加载策略
曝光只是触发器之一
缓存不是可选功能
缓存是首页体验的基础设施
失败不是整个页面失败
失败是组件级降级你的项目建议实现顺序
第一版别直接做太复杂,按这个顺序:
1. HomeLayoutConfig
2. HomeComponentModel
3. HomeComponentRegistry
4. HomeComponentRenderer
5. HomeLoadPlanner
6. HomeComponentLoader
7. HomeLoadScheduler
8. ComponentState 独立管理
9. 缓存 staleWhileRevalidate
10. 曝光/点击埋点你这个项目的首页架构最终应该是:
Config Driven Home
+ Pluggable Component
+ TaskFlow Scheduler
+ Component Level Cache
+ Component Level Fallback这就是比较接近大厂的做法。