數據預拉取的原理其實很簡單,就是在小程序啓動時,微信服務器代理小程序客戶端發起一個 HTTP 請求到第三方服務器來獲取數據,而且把響應數據存儲在本地客戶端供小程序前端調取。當小程序加載完成後,只需調用微信提供的 API wx.getBackgroundFetchData
從本地緩存獲取數據便可。這種作法能夠充分利用小程序啓動和初始化階段的等待時間,使更快地完成頁面渲染。css
京喜小程序首頁已經在生產環境實踐過這個能力,從每日千萬級的數據分析得出,預拉取使冷啓動時獲取到接口數據的時間節點從 2.5s 加速到 1s(提速了 60%)。雖然提高效果很是明顯,但這個能力依然存在一些不成熟的地方:html
因爲預拉取的請求最終是由微信的服務器發起的,也許是出於服務器資源限制的考慮,預拉取的數據會緩存在微信本地一段時間,緩存失效後纔會從新發起請求。通過真機實測,在微信購物入口冷啓動京喜小程序的場景下,預拉取緩存存活了 30 分鐘以上,這對於數據實時性要求比較高的系統來講是很是致命的。前端
因爲請求第三方服務器是從微信的服務器發起的,而不是從小程序客戶端發起的,因此本地代理沒法攔截到這一次真實請求,這會致使開發者沒法經過攔截請求的方式來區分獲取線上環境和開發環境的數據,給開發調試帶來麻煩。git
小程序內部接口的響應體類型都是 application/octet-stream
,即數據格式未知,使本地代理沒法正確解析。github
若是這幾個問題點都不會影響到你的場景,那麼能夠嘗試開啓預拉取能力,這對於小程序首屏渲染速度是質的提高。web
爲了儘快獲取到服務端數據,比較常見的作法是在頁面 onLoad
鉤子被觸發時發起網絡請求,但其實這並非最快的方式。從發起頁面跳轉,到下一個頁面 onLoad
的過程當中,小程序須要完成一些環境初始化及頁面實例化的工做,耗時大概爲 300 ~ 400 毫秒。算法
實際上,咱們能夠在發起跳轉前(如 wx.navigateTo
調用前),提早請求下一個頁面的主接口並存儲在全局 Promise
對象中,待下個頁面加載完成後從 Promise
對象中讀取數據便可。npm
這也是雙線程模型所帶來的優點之一,不一樣於多頁面 web 應用在頁面跳轉/刷新時就銷燬掉 window 對象。小程序
若是開啓了分包加載能力,在用戶訪問到分包內某個頁面時,小程序纔會開始下載對應的分包。當處於分包下載階段時,頁面會維持在 「白屏」 的啓動態,這用戶體驗是比較糟糕的。微信小程序
幸虧,小程序提供了 分包預下載 能力,開發者能夠配置進入某個頁面時預下載可能會用到的分包,避免在頁面切換時僵持在 「白屏」 態。
這是關鍵渲染路徑優化的其中一個思路,從縮短網絡請求時延的角度加快首屏渲染完成時間。
關鍵渲染路徑(Critical Rendering Path) 是指在完成首屏渲染的過程當中必須發生的事件。
以京喜小程序如此龐大的小程序項目爲例,每一個模塊背後均可能有着海量的後臺服務做支撐,而這些後臺服務間的通訊和數據交互都會存在必定的時延。咱們根據京喜首頁的頁面結構,把全部模塊劃分紅兩類:主體模塊(導航、商品輪播、商品豆腐塊等)和 非主體模塊(幕簾彈窗、右側掛件等)。
在初始化首頁時,小程序會發起一個聚合接口請求來獲取主體模塊的數據,而非主體模塊的數據則從另外一個接口獲取,經過拆分的手段來下降主接口的調用時延,同時減小響應體的數據量,縮減網絡傳輸時間。
這也是關鍵渲染路徑優化思路之一,經過延遲非關鍵元素的渲染時機,爲關鍵渲染路徑騰出資源。
相似上一條措施,繼續以京喜小程序首頁爲例,咱們在 主體模塊 的基礎上再度劃分出 首屏模塊(商品豆腐塊以上部分) 和 非首屏模塊(商品豆腐塊及如下部分)。當小程序獲取到主體模塊的數據後,會優先渲染首屏模塊,在全部首屏模塊都渲染完成後纔會渲染非首屏模塊和非主體模塊,以此確保首屏內容以最快速度呈現。
爲了更好地呈現效果,上面 gif 作了降速處理
在小程序中,發起網絡請求是經過 wx.request 這個 API。咱們知道,在 web 瀏覽器中,針對同一域名的 HTTP 併發請求數是有限制的;在小程序中也有相似的限制,但區別在於不是針對域名限制,而是針對 API 調用:
wx.request
(HTTP 鏈接)的最大併發限制是 10 個;wx.connectSocket
(WebSocket 鏈接)的最大併發限制是 5 個;超出併發限制數目的 HTTP 請求將會被阻塞,須要在隊列中等待前面的請求完成,從而必定程度上增長了請求時延。所以,對於職責相似的網絡請求,最好採用節流的方式,先在必定時間間隔內收集數據,再合併到一個請求體中發送給服務端。
圖片資源一直是移動端系統中搶佔大流量的部分,尤爲是對於電商系統。優化圖片資源的加載能夠有效地加快頁面響應時間,提高首屏渲染速度。
WebP 是 Google 推出的一種支持有損/無損壓縮的圖片文件格式,得益於更優的圖像數據壓縮算法,其與 JPG、PNG 等格式相比,在肉眼無差異的圖片質量前提下具備更小的圖片體積(據官方說明,WebP 無損壓縮體積比 PNG 小 26%,有損壓縮體積比 JPEG 小 25-34%)。
小程序的 image 組件 支持 JPG、PNG、SVG、WEBP、GIF 等格式。
鑑於移動端設備的分辨率是有上限的,不少圖片的尺寸經常遠大於頁面元素尺寸,這很是浪費網絡資源(通常圖片尺寸 2 倍於頁面元素真實尺寸比較合適)。得益於京東內部強大的圖片處理服務,咱們能夠經過資源的命名規則和請求參數來獲取服務端優化後的圖片:
裁剪成 100x100 的圖片:https://{host}/s100x100_jfs/{file_path}
;
降質 70%:https://{href}!q70
;
這二者都是比較老生常談的圖片優化技術,這裏就不打算細講了。
小程序的 image 組件 自帶 lazy-load
懶加載支持。雪碧圖技術(CSS Sprite)能夠參考 w3schools 的教程。
在不得不使用大圖資源的場景下,咱們能夠適當使用 「體驗換速度」 的措施來提高渲染性能。
小程序會把已加載的靜態資源緩存在本地,當短期內再次發起請求時會直接從緩存中取資源(與瀏覽器行爲一致)。所以,對於大圖資源,咱們能夠先呈現高度壓縮的模糊圖片,同時利用一個隱藏的 <image>
節點來加載原圖,待原圖加載完成後再轉移到真實節點上渲染。整個流程,從視覺上會感知到圖片從模糊到高清的過程,但與對首屏渲染的提高效果相比,這點體驗落差是能夠接受的。
下面爲你們提供部分例程:
<!-- banner.wxml --> <image src="{{url}}" /> <!-- 圖片加載器 --> <image src="{{preloadUrl}}" bindload="onImgLoad" binderror="onErrorLoad" /> 複製代碼
// banner.js Component({ ready() { this.originUrl = 'https://path/to/picture' // 圖片源地址 this.setData({ url: compress(this.originUrl) // 加載壓縮降質的圖片 preloadUrl: this.originUrl // 預加載原圖 }) }, methods: { onImgLoad() { this.setData({ url: this.originUrl // 加載原圖 }) } } }) 複製代碼
注意,具備display: none
樣式的<image>
標籤只會加載圖片資源,但不渲染。
京喜首頁的商品輪播模塊也採用了這種降級加載方案,在首屏渲染時只會加載第一幀降質圖片。以每幀原圖 20~50kb 的大小計算,這一措施能夠在初始化階段節省掉幾百 kb 的網絡資源請求。
爲了更好地呈現效果,上面 gif 作了降速處理
一方面,咱們能夠從下降網絡請求時延、減小關鍵渲染的節點數這兩個角度出發,縮短完成 FMP(首次有效繪製)的時間。另外一方面,咱們也須要從用戶感知的角度優化加載體驗。
「白屏」 的加載體驗對於首次訪問的用戶來講是難以接受的,咱們可使用尺寸穩定的骨架屏,來輔助實現真實模塊佔位和瞬間加載。
骨架屏目前在業界被普遍應用,京喜首頁選擇使用灰色豆腐塊做爲骨架屏的主元素,大體勾勒出各模塊主體內容的樣式佈局。因爲微信小程序不支持 SSR(服務端渲染),使動態渲染骨架屏的方案難以實現,所以京喜首頁的骨架屏是經過 WXSS 樣式靜態渲染的。
有趣的是,京喜首頁的骨架屏方案經歷了 「統一管理」 和 「(組件)獨立管理」 兩個階段。出於避免對組件的侵入性考慮,最初的骨架屏是由一個完整的骨架屏組件統一管理的:
<!-- index.wxml --> <skeleton wx:if="{{isLoading}}"></skeleton> <block wx:else> 頁面主體 </block> 複製代碼
但這種作法的維護成本比較高,每次頁面主體模塊更新迭代,都須要在骨架屏組件中的對應節點同步更新(譬如某個模塊的尺寸被調整)。除此以外,感官上從骨架屏到真實模塊的切換是跳躍式的,這是由於骨架屏組件和頁面主體節點之間的關係是總體條件互斥的,只有當頁面主體數據 Ready(或渲染完畢)時纔會把骨架屏組件銷燬,渲染(或展現)主體內容。
爲了使用戶感知體驗更加絲滑,咱們把骨架屏元素拆分放到各個業務組件中,骨架屏元素的顯示/隱藏邏輯由業務組件內部獨立管理,這就能夠輕鬆實現 「誰跑得快,誰先出來」 的並行加載效果。除此以外,骨架屏元素與業務組件共用一套 WXML 節點,且相關樣式由公共的 sass
模塊集中管理,業務組件只須要在適當的節點掛上 skeleton
和 skeleton__block
樣式塊便可,極大地下降了維護成本。
<!-- banner.wxml --> <view class="{{isLoading ? 'banner--skeleton' : ''}}"> <view class="banner_wrapper"></view> </view> 複製代碼
// banner.scss .banner--skeleton { @include skeleton; .banner_wrapper { @include skeleton__block; } } 複製代碼
上面的 gif 在壓縮過程有些小問題,你們能夠直接訪問【京喜】小程序體驗骨架屏效果。
當調用 wx.navigateTo
打開一個新的小程序頁面時,小程序框架會完成這幾步工做:
1. 準備新的 webview 線程環境,包括基礎庫的初始化;
2. 從邏輯層到視圖層的初始數據通訊;
3. 視圖層根據邏輯層的數據,結合 WXML 片斷構建出節點樹(包括節點屬性、事件綁定等信息),最終與 WXSS 結合完成頁面渲染;
因爲微信會提早開始準備 webview 線程環境,因此小程序的渲染損耗主要在後二者 數據通訊 和 節點樹建立/更新 的流程中。相對應的,比較有效的渲染性能優化方向就是:
setData
調用儘量地把屢次 setData
調用合併成一次。
咱們除了要從編碼規範上踐行這個原則,還能夠經過一些技術手段下降 setData
的調用頻次。譬如,把同一個時間片(事件循環)內的 setData
調用合併在一塊兒,Taro 框架就使用了這個優化手段。
在 Taro 框架下,調用 setState
時提供的對象會被加入到一個數組中,當下一次事件循環執行的時候再把這些對象合併一塊兒,經過 setData
傳遞給原生小程序。
// 小程序裏的時間片 API const nextTick = wx.nextTick ? wx.nextTick : setTimeout; 複製代碼
data
中不可貴出,setData
傳輸的數據量越多,線程間通訊的耗時越長,渲染速度就越慢。根據微信官方測得的數據,傳輸時間和數據量大致上呈正相關關係:
上圖來自小程序官方開發指南
因此,與視圖層渲染無關的數據儘可能不要放在 data
中,能夠放在頁面(組件)類的其餘字段下。
每當調用 setData
更新數據時,會引發視圖層的從新渲染,小程序會結合新的 data
數據和 WXML 片斷構建出新的節點樹,並與當前節點樹進行比較得出最終須要更新的節點(屬性)。
即便小程序在底層框架層面已經對節點樹更新進行了 diff,但咱們依舊能夠優化此次 diff 的性能。譬如,在調用 setData
時,提早確保傳遞的全部新數據都是有變化的,也就是針對 data 提早作一次 diff。
Taro 框架內部作了這一層優化。在每次調用原生小程序的 setData
以前,Taro 會把最新的 state 和當前頁面實例的 data 作一次 diff,篩選出有必要更新的數據再執行 setData
。
附 Taro 框架的 數據 diff 規則
當用戶事件(如 Click
、Touch
事件等)被觸發時,視圖層會把事件信息反饋給邏輯層,這也是一個線程間通訊的過程。但,若是沒有在邏輯層中綁定事件的回調函數,通訊將不會被觸發。
因此,儘可能減小沒必要要的事件綁定,尤爲是像 onPageScroll
這種會被頻繁觸發的用戶事件,會使通訊過程頻繁發生。
組件節點支持附加自定義數據 dataset
(見下面例子),當用戶事件被觸發時,視圖層會把事件 target
和 dataset
數據傳輸給邏輯層。那麼,當自定義數據量越大,事件通訊的耗時就會越長,因此應該避免在自定義數據中設置太多數據。
<!-- wxml --> <view data-a='A' data-b='B' bindtap='bindViewTap' > Click Me! </view> 複製代碼
// js Page({ bindViewTap(e) { console.log(e.currentTarget.dataset) } }) 複製代碼
小程序的組件模型與 Web Components 標準中的 ShadowDOM 很是相似,每一個組件都有獨立的節點樹,擁有各自獨立的邏輯空間(包括獨立的數據、setData
調用、createSelectorQuery
執行域等)。
不可貴出,若是自定義組件的顆粒度太粗,組件邏輯太重,會影響節點樹構建和新/舊節點樹 diff 的效率,從而影響到組件內 setData
的性能。另外,若是組件內使用了 createSelectorQuery
來查找節點,過於龐大的節點樹結構也會影響查找效率。
咱們來看一個場景,京喜首頁的 「京東秒殺」 模塊涉及到一個倒計時特性,是經過 setInterval
每秒調用 setData
來更新錶盤時間。咱們經過把倒計時抽離出一個基礎組件,能夠有效下降頻繁 setData
時的性能影響。
適當的組件化,既能夠減少數據更新時的影響範圍,又能支持複用,何樂而不爲?誠然,並不是組件顆粒度越細越好,組件數量和小程序代碼包大小是正相關的。尤爲是對於使用編譯型框架(如 Taro)的項目,每一個組件編譯後都會產生額外的運行時代碼和環境 polyfill,so,爲了代碼包空間,請保持理智...
WXML 數據綁定是小程序中父組件向子組件傳遞動態數據的較爲常見的方式,以下面例程所示:Component A
組件中的變量 a
、b
經過組件屬性傳遞給 Component B
組件。在此過程當中,不可避免地須要經歷一次 Component A
組件的 setData
調用方可完成任務,這就會產生線程間的通訊。「合情合理」,但,若是傳遞給子組件的數據只有一部分是與視圖渲染有關呢?
<!-- Component A --> <component-b prop-a="{{a}}" prop-b="{{b}}" /> 複製代碼
// Component B Component({ properties: { propA: String, propB: String, }, methods: { onLoad: function() { this.data.propA this.data.propB } } }) 複製代碼
推薦一種特定場景下很是便捷的作法:經過事件總線(EventBus),也就是發佈/訂閱模式,來完成由父向子的數據傳遞。其構成很是簡單(例程只提供關鍵代碼...):
一個全局的事件調度中心
class EventBus { constructor() { this.events = {} } on(key, cb) { this.events[key].push(cb) } trigger(key, args) { this.events[key].forEach(function (cb) { cb.call(this, ...args) }) } remove() {} } const event = new EventBus() 複製代碼
事件訂閱者
// 子組件 Component({ created() { event.on('data-ready', (data) => { this.setData({ data }) }) } }) 複製代碼
事件發佈者
// Parent Component({ ready() { event.trigger('data-ready', data) } }) 複製代碼
子組件被建立時事先監聽數據下發事件,當父組件獲取到數據後觸發事件把數據傳遞給子組件,這整個過程都是在小程序的邏輯層裏同步執行,比數據綁定的方式速度更快。
但並不是全部場景都適合這種作法。像京喜首頁這種具備 「數據單向傳遞」、「展現型交互」 特性、且 一級子組件數量龐大 的場景,使用事件總線的效益將會很是高;但如果頻繁 「雙向數據流「 的場景,用這種方式會致使事件交錯難以維護。
題外話,Taro 框架在處理父子組件間數據傳遞時使用的是觀察者模式,經過 Object.defineProperty
綁定父子組件關係,當父組件數據發生變化時,會遞歸通知全部後代組件檢查並更新數據。這個通知的過程會同步觸發數據 diff 和一些校驗邏輯,每一個組件跑一遍大概須要 5 ~ 10 ms 的時間。因此,若是組件量級比較大,整個流程下來時間損耗仍是不小的,咱們依舊能夠嘗試事件總線的方案。
咱們可能會遇到這樣的需求,多個組件之間位置不固定,支持隨時隨地靈活配置,京喜首頁也存在相似的訴求。
京喜首頁主體可被劃分爲若干個業務組件(如搜索框、導航欄、商品輪播等),這些業務組件的順序是不固定的,今天是搜索框在最頂部,明天有可能變成導航欄在頂部了(誇張了...)。咱們不可能針對多種順序可能性提供多套實現,這就須要用到小程序的自定義模板 <template>
。
實現一個支持調度全部業務組件的模板,根據後臺下發的模塊數組按序循環渲染模板,以下面例程所示。
<!-- index.wxml --> <template name="render-component"> <search-bar wx:if="{{compId === 'SearchBar'}}" floor-id="{{index}}" /> <nav-bar wx:if="{{compId === 'NavBar'}}" floor-id="{{index}}" /> <banner wx:if="{{compId === 'Banner'}}" floor-id="{{index}}" /> <icon-nav wx:if="{{compId === 'IconNav'}}" floor-id="{{index}}" /> </template> <view class="component-wrapper" wx:for="{{comps}}" wx:for-item="comp" > <template is="render-component" data="{{...comp}}"/> </view> 複製代碼
// search-bar.js Component({ properties: { floorId: Number, }, created() { event.on('data-ready', (comps) => { const data = comps[this.data.floorId] // 根據樓層位置取數據 }) } }) 複製代碼
貌似很是輕鬆地完成需求,但值得思考的是:若是組件順序調整了,全部組件的生命週期會發生什麼變化?
假設,上一次渲染的組件順序是 ['search-bar','nav-bar','banner', 'icon-nav']
,如今須要把 nav-bar
組件去掉,調整爲 ['search-bar','banner', 'icon-nav']
。經實驗得出,當某個組件節點發生變化時,其前面的組件不受影響,其後面的組件都會被銷燬從新掛載。
原理很簡單,每一個組件都有各自隔離的節點樹(ShadowTree
),頁面 body 也是一個節點樹。在調整組件順序時,小程序框架會遍歷比較新/舊節點樹的差別,因而發現新節點樹的 nav-bar
組件節點不見了,就認爲該(樹)分支下從 nav-bar
節點起發生了變化,日後節點都須要重渲染。
但實際上,這裏的組件順序是沒有變化的,丟失的組件按道理不該該影響到其餘組件的正常渲染。因此,咱們在 setData
前先進行了新舊組件列表 diff:若是 newList
裏面的組件是 oldList
的子集,且相對順序沒有發生變化,則全部組件不從新掛載。除此以外,咱們還要在接口數據的相應位置填充上空數據,把該組件隱藏掉,done。
經過組件 diff 的手段,能夠有效下降視圖層的渲染壓力,若是有相似場景的朋友,也能夠參考這種方案。
想必沒有什麼會比小程序 Crash 更影響用戶體驗了。
當小程序佔用系統資源太高,就有可能會被系統銷燬或被微信客戶端主動回收。應對這種尷尬場景,除了提示用戶提高硬件性能以外(譬如來京東商城買新手機),還能夠經過一系列的優化手段下降小程序的內存損耗。
小程序提供了監聽內存不足告警事件的 API:wx.onMemoryWarning,旨在讓開發者收到告警時及時釋放內存資源避免小程序 Crash。然而對於小程序開發者來講,內存資源目前是沒法直接觸碰的,最多就是調用 wx.reLaunch
清理全部頁面棧,重載當前頁面,來下降內存負荷(此方案過於粗暴,別衝動,想一想就好...)。
不過內存告警的信息收集卻是有意義的,咱們能夠把內存告警信息(包括頁面路徑、客戶端版本、終端手機型號等)上報到日誌系統,分析出哪些頁面 Crash 率比較高,從而針對性地作優化,下降頁面複雜度等等。
根據雙線程模型,小程序每個頁面都會獨立一個 webview 線程,但邏輯層是單線程的,也就是全部的 webview 線程共享一個 JS 線程。以致於當頁面切換到後臺態時,仍然有可能搶佔到邏輯層的資源,譬如沒有銷燬的 setInterval
、setTimeout
定時器:
// Page A Page({ onLoad() { let i = 0 setInterval(() => { i++ }, 100) } }) 複製代碼
即便如小程序的
<swiper>
組件,在頁面進入後臺態時依然是會持續輪播的。
正確的作法是,在頁面 onHide
的時候手動把定時器清理掉,有必要時再在 onShow
階段恢復定時器。坦白講,區區一個定時器回調函數的執行,對於系統的影響應該是微不足道的,但不容忽視的是回調函數裏的代碼邏輯,譬如在定時器回調裏持續 setData
大量數據,這就很是難受了...
咱們常常會遇到這樣的需求:廣告曝光、圖片懶加載、導航欄吸頂等等,這些都須要咱們在頁面滾動事件觸發時實時監聽元素位置或更新視圖。在瞭解小程序的雙線程模型以後不難發現,頁面滾動時 onPageScroll
被頻發觸發,會使邏輯層和視圖層發生持續通訊,若這時候再 「火上澆油」 調用 setData
傳輸大量數據,會致使內存使用率快速上升,使頁面卡頓甚至 「假死」。因此,針對頻發事件的監聽,咱們最好遵循如下原則:
onPageScroll
事件回調使用節流;setData
,或減少 setData
的數據量;據 小程序官方文檔 描述,大圖片和長列表圖片在 iOS 中會引發 WKWebView 的回收,致使小程序 Crash。
對於大圖片資源(譬如滿屏的 gif 圖)來講,咱們只能儘量對圖片進行降質或裁剪,固然不使用是最好的。
對於長列表,譬如瀑布流,這裏提供一種思路:咱們能夠利用 IntersectionObserver 監聽長列表內組件與視窗之間的相交狀態,當組件距離視窗大於某個臨界點時,銷燬該組件釋放內存空間,並用等尺寸的骨架圖佔坑;當距離小於臨界點時,再取緩存數據從新加載該組件。
然而無可避免地,當用戶快速滾動長列表時,被銷燬的組件可能來不及加載完,視覺上就會出現短暫的白屏。咱們能夠適當地調整銷燬閾值,或者優化骨架圖的樣式來儘量提高體驗感。
小程序官方提供了一個 長列表組件,能夠經過 npm
包的方式引入,有興趣的能夠嘗試。