小程序的性能问题有很多种:滚动卡顿、列表渲染慢、图片加载慢、内存占用高。但如果只选一个最容易影响用户体感的指标,我会优先看首次加载时间。
用户第一次打开小程序时,还没有形成明确耐心。页面如果长时间白屏,或者一直停留在 loading 状态,很容易直接退出。尤其是资讯、车型、活动这类入口型页面,首屏能否尽快出现,往往比后续某个局部交互优化更关键。
小程序首次加载不是单纯的接口慢,也不是单纯的页面写法问题。它通常由几段链路叠加出来:
下载代码包 -> 初始化运行环境 -> 执行主包代码 -> 拉取首屏数据 -> 渲染首屏结构 -> 加载首屏图片资源
任何一段变慢,用户都会感受到“打开慢”。
首先控制主包体积
小程序启动性能最直接的影响因素是代码包大小。主包越大,首次下载和初始化成本越高。即使后续页面做了缓存,第一次进入时仍然需要先把必要代码加载起来。
因此,优化首次加载时间时,第一件事不是改某个接口,而是看主包里到底放了什么:
是否把低频页面也放进了主包是否引入了整包工具库是否把不属于首屏的组件注册成全局组件是否把大图片、长 Base64 或无关静态资源打进代码包
开发阶段可以持续使用代码包分析工具检查主包构成。主包不应该因为业务迭代自然膨胀,而应该有明确边界:只保留启动、Tab 页、公共基础能力和首屏必须依赖的内容。
分包的目标是让首包足够小
当页面数量和业务能力持续增加后,分包是小程序里最重要的启动优化手段之一。
分包的核心目标不是“为了满足包体积限制”,而是让用户第一次打开时只下载当前必须使用的代码。车型详情、文章详情、图集页、活动页、配置页等低频或后置页面,都不应该无条件进入主包。
一个比较清晰的拆分方式是按业务入口划分:
主包 -> 首页、Tab 页、公共请求、登录态、基础组件资讯分包 -> 文章详情、图集、富文本渲染相关能力车型分包 -> 车型详情、车型图片、360 度展示相关能力活动分包 -> 运营活动、表单承接、专题页面
对应到配置上,可以按页面路径声明分包:
{ "pages": [ "pages/home/index", "pages/profile/index" ], "subpackages": [ { "root": "packages/article", "pages": [ "detail/index", "gallery/index" ] }, { "root": "packages/car", "pages": [ "detail/index", "panorama/index" ] } ]}
这样首页启动时不需要带上文章详情和车型复杂交互代码。用户真正进入这些页面时,再按需加载对应分包。
分包不是越碎越好
分包也有成本。拆得太碎,用户在页面之间跳转时可能频繁等待分包加载;公共代码如果拆分不合理,也可能在多个包之间重复。
比较稳妥的原则是:
- 高频首屏能力留在主包。
- 同一业务链路的页面放在同一个分包。
- 低频、重交互、资源依赖多的页面从主包移出。
- 大型第三方库尽量只被需要它的分包引用。
- 定期检查公共依赖是否被重复打包。
分包优化真正要追求的是“首包轻、跳转顺、依赖不重复”,而不是机械地把每个页面都拆成单独分包。
可以配合分包预下载
有些页面虽然不应该进入主包,但用户进入它们的概率很高。例如用户打开首页后,大概率会进入文章详情或车型详情。这类场景可以考虑分包预下载。
预下载的意义是:不阻塞当前首屏,但在合适时机提前准备后续可能使用的分包。
{ "preloadRule": { "pages/home/index": { "network": "wifi", "packages": [ "packages/article", "packages/car" ] } }}
这里需要克制。预下载不应该把所有分包都提前拉下来,否则等于把主包变小以后,又在启动后立刻把下载压力补回来。更合适的是结合页面路径和用户行为,挑选最可能发生的下一跳。
图片资源不要轻易放进代码包
图片是小程序包体积膨胀的常见原因。很多页面首屏慢,并不是业务代码太多,而是把运营图、背景图、车型图或者大量图标直接放进了代码包。
我们更倾向于这样的资源策略:
大图直接通过 CDN 加载极小且稳定的图片可以考虑内置为 Base64普通图标优先使用组件、字体图标或远程资源代码包内只保留启动必须依赖的小资源
大图通过 CDN 加载,可以让代码包保持轻量,也方便资源独立更新。首屏使用的大图需要关注尺寸、格式和缓存策略,不要直接使用设计稿原图。
极小图片使用 Base64 要非常谨慎。Base64 会进入代码或样式内容,过多使用会直接增加包体积,也不利于独立缓存。它只适合非常小、非常稳定、且能减少一次额外请求的资源。
首屏数据要分层请求
首次加载慢经常被归因于“接口慢”,但更准确的问题是:首屏是否必须等待所有接口都完成。
首页或资讯流页面可以将数据分成三类:
首屏必须数据:页面结构、第一屏列表、关键状态首屏增强数据:推荐标签、运营模块、次要统计后置数据:用户行为、更多分页、低优先级卡片
首屏必须数据应该尽量少、结构稳定、接口聚合清晰。非关键数据可以在首屏出现后再补充,避免让一个低优先级接口阻塞整个页面。
async function loadFirstScreen() { const firstPage = await fetchFeedFirstPage(); setData({ list: firstPage.items, loading: false }); loadSecondaryModules();}
这类优化的重点不是少请求一个接口,而是让用户尽早看到可用内容。
setData 要服务于首屏渲染节奏
小程序中,数据从逻辑层传递到视图层需要成本。首屏阶段如果频繁 setData,会让页面在不断更新中变慢。
常见问题包括:
- 一次接口返回后,对列表逐条
setData。 - 将完整大对象传给视图层,但模板只使用其中少数字段。
- 首屏还没出现,就不断更新埋点、状态和非关键模块。
- 图片列表、富文本节点等大数据结构一次性全部写入。
更合理的方式是合并首屏必要更新:
this.setData({ list: normalizeFirstScreenItems(items), hasMore, loading: false});
同时,传给视图层的数据应该尽量贴近模板实际需要。不要把接口原始结果完整塞进页面状态,再由模板层挑选字段。
首屏图片要有占位和延迟策略
即使大图已经放到 CDN,首屏仍可能被图片拖慢。图片加载策略需要配合页面结构设计:
- 首屏关键图片优先加载,非首屏图片延迟加载。
- 列表图片使用合理尺寸,不加载远超展示尺寸的原图。
- 图片区域预留尺寸,避免加载完成后页面抖动。
- 加载失败时展示兜底图或保持占位,不阻塞正文。
- 对图集、车型大图等资源,在用户进入对应场景后再加载。
对于资讯列表,首屏只需要保证第一屏卡片完整可读;后续滚动区域的图片可以交给懒加载和分页加载处理。
组件和依赖也会影响首包
小程序项目使用框架以后,很容易不知不觉引入过多公共组件。为了方便开发,很多组件被注册到全局,但实际只有少量页面使用。
启动优化中需要检查:
全局组件是否真的全局使用公共组件是否依赖了重型工具库页面是否引入了不参与首屏渲染的复杂组件富文本、图表、地图、视频等能力是否进入主包
对于只在详情页使用的富文本渲染、图集预览、车型 360 度展示等能力,更适合跟随对应业务分包加载。
优化需要用指标闭环
小程序启动优化不能只靠感觉。至少应该持续观察几类指标:
| 指标 | 说明 |
|---|---|
| 主包体积 | 直接影响首次下载和启动成本 |
| 分包体积 | 影响用户进入后续页面时的等待 |
| 首屏数据耗时 | 首屏必要接口是否拖慢页面出现 |
| 首屏渲染时间 | 从进入页面到用户看到可用内容的时间 |
| 图片加载失败率 | CDN、尺寸、网络环境是否存在问题 |
开发阶段可以结合代码包分析、性能扫描和启动耗时埋点定位问题。上线后则需要按页面和网络环境拆分指标,否则一个平均耗时很容易掩盖低端机或弱网用户的真实体验。
小结
小程序首次加载优化的核心是减少“打开时必须完成的事情”:
主包只保留启动必须能力分包承载低频和重交互页面大图走 CDN,小图谨慎内置首屏接口只请求必要数据setData 合并且只传模板需要的数据后置模块和非首屏图片延迟加载
性能优化不是把所有资源都提前准备好,而是让用户最先看到的那部分页面尽快可用。对小程序来说,首包、首屏数据和首屏渲染节奏三者控制住,首次加载体验才会真正稳定下来。