欢迎光临
我们一直在努力

uni-app 小程序首屏要等六秒:分包加载与启动性能优化的完整改造记录

uni-app 小程序首屏要等六秒:分包加载与启动性能优化的完整改造记录

适用读者:用 uni-app 编译微信小程序、主包已经逼近 2MB 上限、手上又是一份不太好动的老工程的前端。文中的数字来自我们项目真机采样,机型是 2019 年的千元安卓机和一台 iPhone 11,换个设备肯定不一样,看趋势别盯着单个数字不放。

上线前最后一次真机验收,我拿那台千元安卓机点开自家小程序,白屏数到六才看到首页列表。同一台机器打开同类型的另一个小程序,两秒出头就出内容了。开发者工具跑了一次体验评分,启动耗时那栏写着 6124ms,评级 D。

主包体积 3.1MB。这个数字一出来,后面所有优化方向基本就定死了:先把包拆小,再谈渲染。

六秒都花在哪了

我先在开发者工具的性能面板上抓了一次冷启动,把耗时按阶段拆开。微信小程序官方把启动分成「代码包下载」「代码注入」「首屏渲染」三段,我们这个工程的分布是这样的:

小程序分包优化主题图:手机加载提速

阶段耗时(ms)占比主要瓶颈
代码包下载 2380 39% 主包 3.1MB,4G 弱网下放大明显
代码注入 1970 32% 所有页面 JS 一次性注入
首屏渲染 1240 20% 首页 onLoad 里串行三个请求
其他(逻辑层初始化等) 534 9% App.onLaunch 同步逻辑

三个数字里最能改的是前两个。下载靠分包,注入靠按需注入(lazyCodeLoading)。第三项属于业务代码问题,得单独收拾。

用图把这条链路画出来更直观:

#mermaid-svg-UUtECIAsld2kSf4s{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UUtECIAsld2kSf4s .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UUtECIAsld2kSf4s .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UUtECIAsld2kSf4s .error-icon{fill:#552222;}#mermaid-svg-UUtECIAsld2kSf4s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UUtECIAsld2kSf4s .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UUtECIAsld2kSf4s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UUtECIAsld2kSf4s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UUtECIAsld2kSf4s .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UUtECIAsld2kSf4s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UUtECIAsld2kSf4s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UUtECIAsld2kSf4s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UUtECIAsld2kSf4s .marker.cross{stroke:#333333;}#mermaid-svg-UUtECIAsld2kSf4s svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UUtECIAsld2kSf4s p{margin:0;}#mermaid-svg-UUtECIAsld2kSf4s .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-UUtECIAsld2kSf4s .cluster-label text{fill:#333;}#mermaid-svg-UUtECIAsld2kSf4s .cluster-label span{color:#333;}#mermaid-svg-UUtECIAsld2kSf4s .cluster-label span p{background-color:transparent;}#mermaid-svg-UUtECIAsld2kSf4s .label text,#mermaid-svg-UUtECIAsld2kSf4s span{fill:#333;color:#333;}#mermaid-svg-UUtECIAsld2kSf4s .node rect,#mermaid-svg-UUtECIAsld2kSf4s .node circle,#mermaid-svg-UUtECIAsld2kSf4s .node ellipse,#mermaid-svg-UUtECIAsld2kSf4s .node polygon,#mermaid-svg-UUtECIAsld2kSf4s .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UUtECIAsld2kSf4s .rough-node .label text,#mermaid-svg-UUtECIAsld2kSf4s .node .label text,#mermaid-svg-UUtECIAsld2kSf4s .image-shape .label,#mermaid-svg-UUtECIAsld2kSf4s .icon-shape .label{text-anchor:middle;}#mermaid-svg-UUtECIAsld2kSf4s .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UUtECIAsld2kSf4s .rough-node .label,#mermaid-svg-UUtECIAsld2kSf4s .node .label,#mermaid-svg-UUtECIAsld2kSf4s .image-shape .label,#mermaid-svg-UUtECIAsld2kSf4s .icon-shape .label{text-align:center;}#mermaid-svg-UUtECIAsld2kSf4s .node.clickable{cursor:pointer;}#mermaid-svg-UUtECIAsld2kSf4s .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UUtECIAsld2kSf4s .arrowheadPath{fill:#333333;}#mermaid-svg-UUtECIAsld2kSf4s .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UUtECIAsld2kSf4s .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UUtECIAsld2kSf4s .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UUtECIAsld2kSf4s .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UUtECIAsld2kSf4s .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UUtECIAsld2kSf4s .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UUtECIAsld2kSf4s .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UUtECIAsld2kSf4s .cluster text{fill:#333;}#mermaid-svg-UUtECIAsld2kSf4s .cluster span{color:#333;}#mermaid-svg-UUtECIAsld2kSf4s div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UUtECIAsld2kSf4s .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UUtECIAsld2kSf4s rect.text{fill:none;stroke-width:0;}#mermaid-svg-UUtECIAsld2kSf4s .icon-shape,#mermaid-svg-UUtECIAsld2kSf4s .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UUtECIAsld2kSf4s .icon-shape p,#mermaid-svg-UUtECIAsld2kSf4s .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UUtECIAsld2kSf4s .icon-shape .label rect,#mermaid-svg-UUtECIAsld2kSf4s .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UUtECIAsld2kSf4s .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UUtECIAsld2kSf4s .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UUtECIAsld2kSf4s :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

用户点击图标

下载主包代码包

主包是否超过 2MB

下载耗时被网络带宽放大

进入代码注入

逻辑层执行 App.onLaunch

首页 Page.onLoad 发起请求

首次 setData 传给视图层

视图层完成首次绘制 TTI

后台按 preloadRule 拉取分包

启动链路的原理剖析:主包体积为什么直接压着 TTI

小程序是双线程模型,逻辑层跑在 AppService 里,视图层是一个 Webview,两边通过 setData 传数据,中间隔着一层 Native 做转发。冷启动时发生的事情按顺序是:

  • Native 拿到小程序信息,去 CDN 拉取主包代码包,这一步耗时和包体积近似线性,弱网下还会叠加 TLS 握手和重试;
  • 代码注入。逻辑层要把主包里所有页面的 JS 全部执行一遍,页面越多、第三方库越大,这一段的 CPU 时间越长;
  • 首屏渲染。执行 App.onLaunch、首页 Page.onLoad,业务逻辑发请求,拿到数据后 setData,视图层才画出第一屏。
  • 关键点在于第二步和第三步都必须等第一步跑完。主包是串行阻塞的,它没有下载完,注入和渲染一行都执行不了。所以主包每多 1MB,TTI 不是「多加一点」,而是把后面所有阶段整体往后推。

    官方文档里给的约束也印证了这点:单个分包/主包不超过 2MB,整个小程序所有分包总和不超过 20MB。我们主包 3.1MB 是历史遗留——早期没做分包,页面全堆在主包,构建工具也没报错,就一路拖到了验收。

    按需注入(lazyCodeLoading)解决的是第二步。默认情况下,小程序启动时会把主包内所有页面的代码都注入一遍,哪怕用户这辈子都不会点开「关于我们」。开启 "lazyCodeLoading": "requiredComponents" 之后,注入范围收敛到「当前页面实际用到的自定义组件和页面代码」,没被访问的页面代码不执行。注意它的判据是组件依赖,不是路由——某个页面被首页的组件引用了,照样会被注入,这一点后面在 vendor.js 治理里还会碰到。

    主包是怎么长到 3MB 的

    拆包之前得先知道里面装了什么。uni-app 编译到微信小程序后,产物在 dist/build/mp-weixin,我用 source-map-explorer 和微信开发者工具的「代码依赖分析」各跑了一遍,主包构成大致是这样:

    内容体积(KB)成因
    vendor.js(含组件库全量) 1120 uView 全量引入,echarts 全量打进来
    业务页面 JS 780 32 个页面全在主包
    静态图片与字体 640 首页 banner、图标字体未走 CDN
    app.wxss 与公共样式 320 组件库样式全量
    app.js 与配置文件 240 全局混入、request 封装

    最大的一块是 vendor.js。uView 在 main.js 里 Vue.use(uView) 全量注册,echarts 为了一个订单趋势图整包引入,moment 只用来格式化两处日期。这些依赖的共同特点是:只有少数页面用得到,却塞进了每个用户都必须下载的主包。

    分包怎么切:主包只留 tabBar 页面

    分包加载(subpackages)的规划原则我们定了两条,执行起来很快:

    • 主包只放 tabBar 页面 + 公共组件 + 全局样式,其余按业务域拆包;
    • 拆出来的包单个控制在 1.5MB 以内,避免预下载时反噬首屏带宽。

    我们按业务域拆成四个:订单(order)、商品(goods)、用户中心(user)、营销活动(activity)。改造后的 pages.json 是这样:

    // 环境:uni-app 3.8.12(Vue 3 + Vite),微信基础库 3.5.5,开发者工具 1.06.2407120
    // pages.json 在 uni-app 中支持注释,这里用 jsonc 标注每一处改动意图
    {
    // 主包只保留四个 tabBar 页面,其余全部下沉到分包
    "pages": [
    { "path": "pages/index/index", "style": { "navigationBarTitleText": "首页" } },
    { "path": "pages/category/category", "style": { "navigationBarTitleText": "分类" } },
    { "path": "pages/cart/cart", "style": { "navigationBarTitleText": "购物车" } },
    { "path": "pages/mine/mine", "style": { "navigationBarTitleText": "我的" } }
    ],
    // 按业务域划分四个分包,root 目录与业务目录一一对应
    // 每个分包的 root 目录必须在磁盘上真实存在,否则编译报路径找不到
    "subPackages": [
    // 订单域:趋势图依赖 echarts,随分包一起打包,不进主包
    { "root": "pagesOrder", "pages": [{ "path": "list/index" }, { "path": "detail/index" }] },
    { "root": "pagesGoods", "pages": [{ "path": "detail/index" }, { "path": "search/index" }] },
    { "root": "pagesUser", "pages": [{ "path": "setting/index" }, { "path": "address/index" }] },
    // 营销活动页只在活动期间访问,单独拆包避免污染主包
    { "root": "pagesActivity", "pages": [{ "path": "seckill/index" }] }
    ],
    // 按需注入:只注入当前页面用到的组件与页面代码
    "lazyCodeLoading": "requiredComponents",
    // 进入首页后,网络空闲时后台预下载订单与商品分包
    "preloadRule": {
    "pages/index/index": { "network": "all", "packages": ["pagesOrder", "pagesGoods"] }
    },
    "globalStyle": { "navigationBarTextStyle": "black" }
    }

    这里有个坑值得单独说:subPackages 里的页面路径必须放在 root 对应的目录下,而且跨分包跳转要写全路径 /pagesOrder/list/index。我们项目里有一百多处 uni.navigateTo 写的是相对的老路径,拆完包之后全部 404。最后是写了个脚本扫 pages.json 生成路径映射表,配合编辑器的全局替换一次性改完的,手动改必漏。

    preloadRule 用不好会帮倒忙

    预下载(preloadRule)的逻辑是:进入某个页面后,框架在后台悄悄把指定分包拉下来,用户真点进去时就没有下载等待了。听起来很美,但它是和首屏抢带宽的。

    我第一次配的时候图省事写了 "network": "all",packages 里塞了三个分包。结果真机一测,首屏反而慢了 400ms——用户在 4G 下打开首页,主包还没渲完,后台已经开始拖 1.2MB 的分包,首屏的请求被挤到后面排队。

    改造前后的时序差别,画出来是这样:

    用户视图层 Webview逻辑层 AppService微信CDN用户视图层 Webview逻辑层 AppService微信CDN#mermaid-svg-ZCvqRrU1MjKuT3FI{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZCvqRrU1MjKuT3FI .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZCvqRrU1MjKuT3FI .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZCvqRrU1MjKuT3FI .error-icon{fill:#552222;}#mermaid-svg-ZCvqRrU1MjKuT3FI .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZCvqRrU1MjKuT3FI .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZCvqRrU1MjKuT3FI .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZCvqRrU1MjKuT3FI .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZCvqRrU1MjKuT3FI .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZCvqRrU1MjKuT3FI .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZCvqRrU1MjKuT3FI .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZCvqRrU1MjKuT3FI .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZCvqRrU1MjKuT3FI .marker.cross{stroke:#333333;}#mermaid-svg-ZCvqRrU1MjKuT3FI svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZCvqRrU1MjKuT3FI p{margin:0;}#mermaid-svg-ZCvqRrU1MjKuT3FI .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZCvqRrU1MjKuT3FI text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-ZCvqRrU1MjKuT3FI .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZCvqRrU1MjKuT3FI .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-ZCvqRrU1MjKuT3FI .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-ZCvqRrU1MjKuT3FI .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-ZCvqRrU1MjKuT3FI #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-ZCvqRrU1MjKuT3FI .sequenceNumber{fill:white;}#mermaid-svg-ZCvqRrU1MjKuT3FI #sequencenumber{fill:#333;}#mermaid-svg-ZCvqRrU1MjKuT3FI #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-ZCvqRrU1MjKuT3FI .messageText{fill:#333;stroke:none;}#mermaid-svg-ZCvqRrU1MjKuT3FI .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZCvqRrU1MjKuT3FI .labelText,#mermaid-svg-ZCvqRrU1MjKuT3FI .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-ZCvqRrU1MjKuT3FI .loopText,#mermaid-svg-ZCvqRrU1MjKuT3FI .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-ZCvqRrU1MjKuT3FI .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-ZCvqRrU1MjKuT3FI .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-ZCvqRrU1MjKuT3FI .noteText,#mermaid-svg-ZCvqRrU1MjKuT3FI .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-ZCvqRrU1MjKuT3FI .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZCvqRrU1MjKuT3FI .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZCvqRrU1MjKuT3FI .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-ZCvqRrU1MjKuT3FI .actorPopupMenu{position:absolute;}#mermaid-svg-ZCvqRrU1MjKuT3FI .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-ZCvqRrU1MjKuT3FI .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-ZCvqRrU1MjKuT3FI .actor-man circle,#mermaid-svg-ZCvqRrU1MjKuT3FI line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-ZCvqRrU1MjKuT3FI :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}首屏完成后延迟 1.5s 再触发预下载冷启动拉取主包主包 1.42MB(改造前 3.1MB)按需注入首页所需组件首屏 setData(裁剪字段后 22KB)骨架屏占位,1.1s 出现数据到位,替换骨架preloadRule 拉取 pagesOrder 分包后台写入本地缓存点击订单入口直接注入本地代码,180ms 出页面

    调整方式很土但有效:

  • packages 里只放用户最可能马上点进的 1 个包,我们选了订单;
  • 首页的 onReady 之后延迟 1.5 秒再触发,避开首屏请求的高峰;
  • 弱网信号差的场景用 "network": "wifi" 兜底,别拿用户的流量赌。
  • 改完这三点,预下载的收益才转正:从首页点进订单列表,二次打开耗时从 900ms 降到 180ms 左右。

    vendor.js 的三刀

    分包解决的是「页面 JS 放哪」,vendor.js 解决的是「公共依赖有多大」。我们砍了三刀:

    • 组件库按需引入。uView 改成 easycom 自动按需,只把用到的 u-button、u-empty 之类的组件打进去,样式也跟着走按需;
    • 大依赖移入分包。echarts 只有订单详情的趋势图用,直接把 ec-canvas 和 echarts 源码挪到 pagesOrder 目录下,构建时它自然被归到分包产物里;
    • 替换掉过重的小库。moment 换成 dayjs,dayjs 的 locale 再单独按需引。

    // 环境:uni-app 3.8.12(Vue 3 + Vite),微信基础库 3.5.5
    // 文件:src/main.js —— 组件库按需引入改造
    import { createSSRApp } from 'vue'
    import App from './App.vue'

    // 不再 Vue.use(全量组件库),全量注册会把所有组件打进主包 vendor.js
    // easycom 会在编译期按模板里真实用到的标签引入对应组件
    import uButton from 'uview-plus/components/u-button/u-button.vue'
    import uEmpty from 'uview-plus/components/u-empty/u-empty.vue'

    // dayjs 替代 moment:体积从 68KB 降到 6KB,locale 单独按需加载
    import dayjs from 'dayjs'
    import 'dayjs/locale/zh-cn'
    dayjs.locale('zh-cn')

    export function createApp() {
    const app = createSSRApp(App)
    // 只注册首页与 tabBar 页真正用到的组件
    app.component('u-button', uButton)
    app.component('u-empty', uEmpty)
    // 挂载全局属性,替代原先在每个页面重复 import 的写法
    app.config.globalProperties.$dayjs = dayjs
    return { app }
    }

    # 环境:Node 18.19.0,uni-app 3.8.12 构建产物目录 dist/build/mp-weixin
    # 1) 构建发行版小程序(开启压缩与 tree shaking)
    npm run build:mp-weixin
    # 2) 用 source-map-explorer 看 vendor.js 里谁占地方
    npx source-map-explorer dist/build/mp-weixin/vendor.js –html > vendor-report.html
    # 3) 只看主包体积,确认是否降到 2MB 以下
    du -sh dist/build/mp-weixin
    # 4) 逐个分包核对,单个分包不建议超过 1.5MB
    du -sh dist/build/mp-weixin/pagesOrder

    第三刀执行完,vendor.js 从 1120KB 掉到 386KB。这块的收益比预期大,因为组件库全量注册本来就是历史的偷懒写法,跟分包本身没关系,纯粹是欠账。

    剩下的一秒:setData 和骨架屏

    包拆完之后,注入耗时从 1970ms 降到 810ms,但首屏渲染那 1240ms 基本没动。问题有两个。

    第一个是 setData 一次传了太多数据。首页列表接口返回 20 条记录,每条 14 个字段,其中 description 字段平均 300 多个字符,列表渲染根本用不到。我在 onLoad 里做了一层字段裁剪,只保留列表需要的 6 个字段,同时把 20 条改成首屏 8 条 + 上拉加载。这次 setData 的数据量从 180KB 降到 22KB,逻辑层到视图层的传输时间从 310ms 降到 40ms 上下。

    第二个是请求串行。首页原来依次发了 banner、分类、列表三个请求,后一个等前一个。改成 Promise.all 并发之后,等待时间取决于最慢的那个而不是三者之和,省了大约 380ms。

    骨架屏(skeleton)是最后加的。它不改变真实耗时,但把「白屏」变成「有结构感的占位」,用户感知上的等待短了一大截。我们在首页放了一个与真实列表结构一致的骨架组件,数据到位后直接替换。

    改造前后对照

    真机采样 10 次取中位数,同一台千元安卓机、同一个 4G 网络环境:

    指标改造前改造后变化
    主包体积 3.1MB 1.42MB -54%
    分包总大小 2.3MB(4 个包)
    代码包下载 2380ms 1020ms -57%
    代码注入 1970ms 810ms -59%
    首屏渲染 1240ms 620ms -50%
    冷启动总耗时 6124ms 2510ms -59%
    体验评分 D B 提升两档

    冷启动没做到业内常说的 2 秒以内,主要卡在首屏那三个请求上,下一步打算做接口合并和本地缓存兜底。但 6.1 秒到 2.5 秒这个跨度,够用户从「想关掉」变成「愿意等一下」。

    容易踩的几个认知误区

    分包不是越多越好。 分包数量上去之后,跨包跳转的路径管理、公共依赖的重复打包都会变麻烦。我们中途试过拆成 9 个包,结果公共工具函数在每个包里各存一份,总体积反而涨了 200KB,最后收回 4 个。

    预下载不等于提前加载业务逻辑。 preloadRule 只下载代码,不执行。页面代码依然要等真正跳转时才注入,所以「预下载完再点进去还是有一小段空白」是正常的,别拿这个去质疑配置没生效。

    按需注入救不了 vendor.js。 lazyCodeLoading 的判据是组件是否被引用,vendor.js 属于公共模块,只要首页间接引用了就会被注入。这也就是为什么我们把它和分包拆成两件事来做:一个管页面,一个管依赖。

    往后看,小程序启动性能的优化空间会越来越集中在首屏数据链路上,包体积这块能挤的水分其实就那么多。基础库新版本对代码注入做了不少底层优化,把基础库最低版本往上抬一抬,往往比自己折腾半天收益更大。

    另外提醒一句:这些数字记得在开发者工具里用「真机调试」抓,本地模拟器的网络和 CPU 跟真机完全不是一回事,我一开始在本地跑出来的启动耗时只有 1.8 秒,差点以为问题不存在。

    你们项目里主包最大的那一块是什么?评论区聊聊,我看到都会回。

    参考与延伸

    • 微信开放文档 · 分包加载:https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic
    • 微信开放文档 · 性能优化与体验评分:https://developers.weixin.qq.com/miniprogram/dev/framework/performance/report
    • 微信开放文档 · 按需注入与用时注入:https://developers.weixin.qq.com/miniprogram/dev/framework/ability/lazyload.html
    • uni-app 官方文档 · pages.json 页面路由:https://uniapp.dcloud.net.cn/collocation/pages.html

    微信小程序开发 / 小程序性能优化 / 分包加载 / lazyCodeLoading / 首屏加载 / 启动耗时 / uni-app 踩坑
    ttps://developers.weixin.qq.com/miniprogram/dev/framework/ability/lazyload.html

    • uni-app 官方文档 · pages.json 页面路由:https://uniapp.dcloud.net.cn/collocation/pages.html

    微信小程序开发 / 小程序性能优化 / 分包加载 / lazyCodeLoading / 首屏加载 / 启动耗时 / uni-app 踩坑

    赞(0)
    未经允许不得转载:171主机测评 » uni-app 小程序首屏要等六秒:分包加载与启动性能优化的完整改造记录
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址