你的微信小程序首屏打開(kāi)時(shí)間超過(guò) 3 秒,每增加 1 秒就會(huì)有 11% 的用戶流失——這是微信官方在 2024 年開(kāi)發(fā)者論壇上再次引用的行為監(jiān)測(cè)數(shù)據(jù)。首屏加載慢的直接后果不是“體驗(yàn)稍差”,而是不可逆的用戶放棄。這篇文章面向正在維護(hù)或評(píng)估小程序性能的開(kāi)發(fā)者,拆解首屏耗時(shí)構(gòu)成,給出三種可并行使用的優(yōu)化路徑,并說(shuō)明每種路徑的邊界條件和取舍。

診斷:首屏?xí)r間都花在哪了

小程序啟動(dòng)到首屏渲染完成,主要時(shí)間消耗在四個(gè)階段:

  1. 小程序環(huán)境初始化:微信客戶端加載小程序基礎(chǔ)庫(kù)、準(zhǔn)備 JS 運(yùn)行環(huán)境。這一階段開(kāi)發(fā)者能控制的幅度很小,但基礎(chǔ)庫(kù)版本的選擇會(huì)影響注入耗時(shí)。
  2. 代碼包下載與注入:微信下載主包(或獨(dú)立分包),并將 JavaScript 代碼注入運(yùn)行環(huán)境。小程序主包體積是這一階段的核心約束——主包超過(guò) 2 MB 后,下載和注入時(shí)間都會(huì)非線性增長(zhǎng)。
  3. 首屏頁(yè)面邏輯執(zhí)行:Page.onLoad、Page.onShow 中的業(yè)務(wù)邏輯,包括數(shù)據(jù)請(qǐng)求、數(shù)據(jù)處理、setData 調(diào)用等。同步阻塞操作或未緩存的網(wǎng)絡(luò)請(qǐng)求會(huì)直接延長(zhǎng)該階段。
  4. 首屏渲染:初始渲染數(shù)據(jù)到達(dá)后,框架完成虛擬 DOM 構(gòu)建并進(jìn)行 UI 線程與邏輯線程的序列化通信,最終上屏。setData 數(shù)據(jù)量和節(jié)點(diǎn)層級(jí)直接影響渲染時(shí)長(zhǎng)。

你可以通過(guò)小程序后臺(tái)的“性能監(jiān)控”模塊,或調(diào)用 wx.getPerformance() 接口,獲取啟動(dòng)各階段的耗時(shí)數(shù)據(jù)。典型的一次緩慢啟動(dòng)日志表現(xiàn)為:代碼包下載耗時(shí) 1200 ms、頁(yè)面首次渲染耗時(shí) 900 ms、總啟動(dòng)耗時(shí) 4200 ms。下面的優(yōu)化方案就圍繞如何縮小這些數(shù)字展開(kāi)。

優(yōu)化:三條核心路徑

1. 用好分包與預(yù)加載,壓縮主包下載量

分包是微信小程序內(nèi)置的代碼組織機(jī)制:將非首屏頁(yè)面的代碼劃入分包(subpackage),使主包只保留啟動(dòng)必需的頁(yè)面和公共資源。你需要先檢查 app.json 中哪些頁(yè)面被強(qiáng)制保留在主包,然后將低頻頁(yè)面剝離。

典型的分包配置如下:

{
  "pages": [
    "pages/index/index",
    "pages/common/error"
  ],
  "subPackages": [
    {
      "root": "packageOrder",
      "pages": [
        "pages/detail/detail",
        "pages/list/list"
      ]
    },
    {
      "root": "packageProfile",
      "pages": [
        "pages/setting/setting"
      ],
      "independent": true
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "network": "wifi",
      "packages": ["packageOrder"]
    }
  }
}

這里做了三件事:

  • 將訂單相關(guān)頁(yè)面移入 packageOrder,個(gè)人中心頁(yè)面移入獨(dú)立分包 packageProfile
  • 設(shè)置預(yù)加載規(guī)則:當(dāng)用戶進(jìn)入首頁(yè)且處于 Wi-Fi 環(huán)境時(shí),后臺(tái)靜默下載 packageOrder。
  • 使用 "independent": true 聲明 packageProfile 為獨(dú)立分包,使其可脫離主包單獨(dú)運(yùn)行,但這也意味著它無(wú)法直接訪問(wèn)主包資源,需要你顯式傳遞依賴。

分包預(yù)加載的觸發(fā)時(shí)機(jī)是 onLoad 后,由框架根據(jù) preloadRule 自動(dòng)執(zhí)行。你還可以在業(yè)務(wù)邏輯中主動(dòng)調(diào)用 wx.preloadSubpackage,例如在訂單列表頁(yè)入口出現(xiàn)時(shí):

// 在可能進(jìn)入訂單頁(yè)面的按鈕事件中觸發(fā)預(yù)加載
btnTap() {
  wx.preloadSubpackage({
    name: 'packageOrder',
    success() {
      console.log('packageOrder 預(yù)加載完成');
    }
  });
}

2. 啟動(dòng)數(shù)據(jù)預(yù)拉取與渲染減負(fù)

首屏往往需要請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)才能完成渲染,網(wǎng)絡(luò)往返時(shí)間直接嵌入初屏耗時(shí)。微信提供 DataPrefetcher(啟動(dòng)數(shù)據(jù)預(yù)拉?。┠芰?,在啟動(dòng)流程中并行發(fā)起請(qǐng)求:你在小程序管理后臺(tái)配置接口地址,微信會(huì)在代碼注入前發(fā)起請(qǐng)求,并將結(jié)果注入到 App.onLaunch 或首頁(yè)的 onLoad 參數(shù)中。這一能力可使數(shù)據(jù)請(qǐng)求與代碼包下載并行,省去一個(gè) RTT。

對(duì)于已經(jīng)拿到數(shù)據(jù)后的渲染,減少 setData 的負(fù)載是關(guān)鍵。你應(yīng)該遵循兩個(gè)原則:

  • 只傳輸變化的數(shù)據(jù),不要將整個(gè)頁(yè)面數(shù)據(jù)對(duì)象全量 setData。
  • 扁平化數(shù)據(jù)結(jié)構(gòu),避免深層級(jí)對(duì)象更新觸發(fā)大面積 diff。

下面是一個(gè)對(duì)比示例,左側(cè)是常見(jiàn)但低效的寫(xiě)法,右側(cè)是優(yōu)化后的寫(xiě)法:

// ? 全量更新,且數(shù)據(jù)嵌套太深
this.setData({
  orderInfo: {
    ...this.data.orderInfo,
    items: newItems
  }
});

// ? 只更新受影響的字段
this.setData({
  'orderInfo.items': newItems
});

除了數(shù)據(jù)層面,你還需要控制首屏渲染的節(jié)點(diǎn)數(shù)量。骨架屏在這個(gè)階段的作用并非“美化等待”,而是先用極簡(jiǎn)的占位節(jié)點(diǎn)替代業(yè)務(wù)組件,讓白屏?xí)r間轉(zhuǎn)化為有意義的視覺(jué)反饋。你可以通過(guò)微信開(kāi)發(fā)者工具的“生成骨架屏”功能自動(dòng)產(chǎn)出占位 WXML,但務(wù)必在真實(shí)數(shù)據(jù)就位后銷毀骨架屏,避免產(chǎn)生不必要的節(jié)點(diǎn)和重繪。

3. 啟動(dòng)流程中的非首屏邏輯延遲執(zhí)行

App.onLaunch 和首頁(yè) onLoad 中經(jīng)常堆積大量與首幀渲染無(wú)關(guān)的任務(wù),比如登錄態(tài)校驗(yàn)、埋點(diǎn)初始化、第三方 SDK 加載等。你應(yīng)該將這些任務(wù)從同步執(zhí)行改為異步延遲,確保不阻塞 onReady 之后的首次渲染。

一種可驗(yàn)證的寫(xiě)法是利用 wx.nextTicksetTimeout 將非首屏邏輯推到下一空閑幀:

Page({
  onLoad() {
    // 即刻需要的:請(qǐng)求首頁(yè)數(shù)據(jù)
    this.fetchHomeData();

    // 推遲執(zhí)行:初始化性能埋點(diǎn)
    wx.nextTick(() => {
      this.initPerfTracker();
    });
  }
});

更嚴(yán)格的控制可以通過(guò) wx.getPerformance() 結(jié)合時(shí)間戳,在啟動(dòng)完成的 appLaunch 節(jié)點(diǎn)之后再觸發(fā)這些邏輯。注意,不能用 setTimeout(fn, 0) 大量拆分任務(wù),這反而可能因?yàn)槭录h(huán)碎片化增加總耗時(shí);優(yōu)先使用框架提供的 wx.nextTickrequestIdleCallback(通過(guò)自行 polyfill 實(shí)現(xiàn)),讓非關(guān)鍵任務(wù)只在渲染空閑時(shí)執(zhí)行。

邊界與取舍

這三條路徑都附帶約束,你需要根據(jù)小程序的實(shí)際業(yè)務(wù)形態(tài)做出取舍。

分包大小的硬限制:?jiǎn)蝹€(gè)分包不能超過(guò) 2 MB,整個(gè)小程序所有分包的總大小不能超過(guò) 20 MB。如果訂單包接近 2 MB 上限,你不能再往里堆積新的頁(yè)面,而要考慮拆分出第二個(gè)訂單分包,或者將部分靜態(tài)資源上云并在頁(yè)面中使用遠(yuǎn)程鏈接。主包同樣受 2 MB 限制,且主包包含所有公共組件和 util 代碼,你需要定期使用“代碼依賴分析”工具剔除未引用的模塊。

預(yù)加載的觸發(fā)條件與流量代價(jià)preloadRule 中的 network 字段可以設(shè)為 "all""wifi"。選擇 "all" 會(huì)在移動(dòng)網(wǎng)絡(luò)下預(yù)加載,優(yōu)化了慢網(wǎng)啟動(dòng)體驗(yàn),但增加了用戶的流量消耗,可能引發(fā)投訴。此外,預(yù)加載規(guī)則只對(duì)普通分包有效,獨(dú)立分包無(wú)法被主包預(yù)加載。如果你選擇了獨(dú)立分包來(lái)完全隔離用戶中心的代碼,就要接受它首次進(jìn)入時(shí)的額外下載耗時(shí)。

啟動(dòng)數(shù)據(jù)預(yù)拉取的局限性:預(yù)拉取接口只能在微信后臺(tái)配置,不支持運(yùn)行時(shí)動(dòng)態(tài)變更 URL,且返回?cái)?shù)據(jù)大小限制在 256 KB 以內(nèi)。適合放置首頁(yè)首屏的最小必要數(shù)據(jù)集,而不是完整的數(shù)據(jù)快照。當(dāng)接口失敗或超時(shí)時(shí),你需要有降級(jí)方案,在頁(yè)面內(nèi)重新發(fā)起請(qǐng)求。

骨架屏與業(yè)務(wù)復(fù)雜度的平衡:如果首屏組件數(shù)量超過(guò) 20 個(gè),自動(dòng)生成的骨架屏模板也可能體積龐大,甚至超過(guò)真實(shí)內(nèi)容所需的 WXML 節(jié)點(diǎn)數(shù)。這時(shí)你應(yīng)考慮減少首屏組件,將非可視區(qū)域(“首屏”指用戶初次看到的視口區(qū)域)內(nèi)容延遲渲染,而不是用一個(gè)復(fù)雜骨架屏去模擬它。

行動(dòng)建議

  1. 先度量,再優(yōu)化:在小程序后臺(tái)打開(kāi)性能監(jiān)控,或使用 wx.getPerformance() 獲取當(dāng)前用戶的首屏?xí)r間分布(下載耗時(shí) + 注入耗時(shí) + 首個(gè)頁(yè)面渲染耗時(shí))。找不到瓶頸就開(kāi)刀,只會(huì)增加維護(hù)負(fù)擔(dān)。
  2. 從主包減肥開(kāi)始:主包體積是最容易放大首屏耗時(shí)的因素。將非首屏頁(yè)面移入分包,剔除未使用的 npm 包和公共樣式,爭(zhēng)取將主包控制在 1 MB 以內(nèi)。
  3. 接著啟用預(yù)拉取與預(yù)加載:在配置了首屏數(shù)據(jù)預(yù)拉取后,首屏數(shù)據(jù)請(qǐng)求不再阻塞渲染流程。同時(shí)為高概率訪問(wèn)的二級(jí)頁(yè)面配置預(yù)加載,讓用戶點(diǎn)擊時(shí)直接打開(kāi)。
  4. 最后優(yōu)化渲染路徑:審查首頁(yè) onLoad 中的同步邏輯,將非首幀任務(wù)推后,并使用骨架屏為用戶提供即時(shí)反饋。監(jiān)控 setData 數(shù)據(jù)量,每次傳輸盡量控制在 64 KB 以下。
  5. 制定長(zhǎng)期守則:將首屏?xí)r間納入版本發(fā)布的性能準(zhǔn)入標(biāo)準(zhǔn),建立回歸監(jiān)控。規(guī)定每次迭代后主包體積不可增長(zhǎng)超過(guò) 5%,否則必須重走拆分流程。

首屏加載之所以成為微信小程序開(kāi)發(fā)的持續(xù)性挑戰(zhàn),是因?yàn)樾枨笈蛎浐痛a膨脹不斷推回原來(lái)的性能基線。但只要你建立了一套可重復(fù)的度量-拆分-預(yù)取-延遲執(zhí)行流程,就能把啟動(dòng)速度控制在用戶愿意等待的紅線之內(nèi)。

← 上一篇 小程序開(kāi)發(fā)選型:跨平臺(tái)框架能幫你省掉一半人力嗎? 下一篇 → 模板小程序走到增長(zhǎng)天花板,定制開(kāi)發(fā)如何成為你的轉(zhuǎn)折點(diǎn)?