餓了么的PWA升級(jí)實(shí)踐

姓名:郭金? 學(xué)號(hào):17101223407

轉(zhuǎn)載自:http://mp.weixin.qq.com/s/nkFDL__jEY07pMCbP3xaqQ

【嵌牛導(dǎo)讀】: PWA作為下一代 Web 應(yīng)用模型,其嘗試解決的是Web平臺(tái)本身的根本性問(wèn)題:對(duì)網(wǎng)絡(luò)與瀏覽器UI的硬依賴(lài)。因此,任何Web應(yīng)用都可以從中獲益,這與你是多頁(yè)還是單頁(yè)、面向桌面還是移動(dòng)端、是用React還是Vue.js無(wú)關(guān)?;蛟S,它還終將改變用戶(hù)對(duì)移動(dòng)Web的期待。

【嵌牛鼻子】:PWA、多頁(yè)應(yīng)用

【嵌牛提問(wèn)】:現(xiàn)如今,誰(shuí)還覺(jué)得桌面端的Web只是個(gè)看文檔的地方呢?

【嵌牛正文】:

? ? 自Vue.js在官方推特第一次公開(kāi)到現(xiàn)在,我們就一直在進(jìn)行著將餓了么移動(dòng)端網(wǎng)站升級(jí)為 Progressive Web App的工作。直到近日在GoogleI/O 2017上登臺(tái)亮相,才終于算告一段落。我們非常榮幸能夠發(fā)布全世界第一個(gè)專(zhuān)門(mén)面向國(guó)內(nèi)用戶(hù)的PWA,但更榮幸的是能與 Google、 UC以及騰訊合作,一起推動(dòng)國(guó)內(nèi)Web與瀏覽器生態(tài)的發(fā)展。

多頁(yè)應(yīng)用、 Vue.js、 PWA?

? ? 對(duì)于構(gòu)建一個(gè)希望達(dá)到原生應(yīng)用級(jí)別體驗(yàn)的PWA,目前社區(qū)里的主流做法都是采用SPA,即 單頁(yè)面應(yīng)用模型(Single-page App)來(lái)組織整個(gè)Web應(yīng)用,業(yè)內(nèi)最有名的幾個(gè)PWA案例Twitter Lite、 Flipkart Lite、Housing Go 與 Polymer Shop無(wú)一例外。

? ? ? 然而餓了么,與很多國(guó)內(nèi)的電商網(wǎng)站一樣,青睞多頁(yè)面應(yīng)用模型(MPA, Multi-page App)所能帶來(lái)的一些好處,也因此在一年多前就將移動(dòng)站從基于AngularJS的單頁(yè)應(yīng)用重構(gòu)為目前的多頁(yè)應(yīng)用模型。團(tuán)隊(duì)最看重的優(yōu)點(diǎn)莫過(guò)于頁(yè)面與頁(yè)面之間的隔離與解耦,這使得我們可以將每個(gè)頁(yè)面當(dāng)做一個(gè)獨(dú)立的“微服務(wù)”來(lái)看待,這些服務(wù)可以被獨(dú)立迭代,獨(dú)立提供給各種第三方的入口嵌入,甚至被不同的團(tuán)隊(duì)獨(dú)立維護(hù)。而整個(gè)網(wǎng)站則只是各種服務(wù)的集合而非一個(gè)巨大的整體。

? ? ? 與此同時(shí),我們?nèi)匀灰蕾?lài) Vue.js作為JavaScript框架。 Vue.js除了是React、 AngularJS這種“重型武器”的競(jìng)爭(zhēng)對(duì)手外,其輕量與高性能的優(yōu)點(diǎn)使得它同樣可以作為傳統(tǒng)多頁(yè)應(yīng)用開(kāi)發(fā)中流行的“jQuery/Zepto/Kissy+模板引擎”技術(shù)棧的完美替代。 Vue.js提供的組件系統(tǒng)、聲明式與響應(yīng)式編程更是提升了代碼組織、共享、數(shù)據(jù)流控制、渲染等各個(gè)環(huán)節(jié)的開(kāi)發(fā)效率。 Vue 還是一個(gè)漸進(jìn)式框架,如果網(wǎng)站的復(fù)雜度繼續(xù)提升,我們可以按需、增量地引入Vuex或Vue-Router這些模塊。萬(wàn)一哪天又要改回單頁(yè)呢?(誰(shuí)知道呢……)

? ? ? 2017年, PWA已經(jīng)成為Web應(yīng)用新的風(fēng)潮。我們決定試試,以我們現(xiàn)有的“Vue.js+多頁(yè)”架構(gòu),能在升級(jí)PWA的道路上走多遠(yuǎn),達(dá)到怎樣的效果。

實(shí)現(xiàn)“PRPL”模式

? ? “PRPL”(讀作“purple”)是Google工程師提出的一種Web應(yīng)用架構(gòu)模式,它旨在利用現(xiàn)代Web平臺(tái)的新技術(shù)以大幅優(yōu)化移動(dòng)Web的性能與體驗(yàn),對(duì)如何組織與設(shè)計(jì)高性能的PWA系統(tǒng)提供了一種高層次的抽象。我們并不準(zhǔn)備從頭重構(gòu)我們的Web應(yīng)用,不過(guò)我們可以把實(shí)現(xiàn)“PRPL”模式作為我們的遷移目標(biāo)?!癙RPL”實(shí)際上是“Push/Preload、 Render、 Precache、 Lazy-Load”的縮寫(xiě),我們接下來(lái)會(huì)展開(kāi)介紹它們的具體含義。

? ? ? Push/Preload,推送/預(yù)加載初始URL路由所需的關(guān)鍵資源

? ? ? 無(wú)論是HTTP2 Server Push還是,其關(guān)鍵都在于,我們希望提前請(qǐng)求一些隱藏在應(yīng)用依賴(lài)關(guān)系(Dependency Graph)較深處的資源,以節(jié)省HTTP往返、瀏覽器解析文檔,或腳本執(zhí)行的時(shí)間。比如說(shuō),對(duì)于一個(gè)基于路由進(jìn)行code splitting的SPA,如果我們可以在Webpack清單、路由等入口代碼(entry chunks)被下載與運(yùn)行之前就把初始URL,即用戶(hù)訪問(wèn)的入口URL路由所依賴(lài)的代碼用Server Push推送或進(jìn)行提前加載。那么當(dāng)這些資源被真正請(qǐng)求時(shí),它們可能已經(jīng)下載好并存在緩存中了,這樣就加快了初始路由所有依賴(lài)的就緒。

? ? ? ? 在多頁(yè)應(yīng)用中,每一個(gè)路由本來(lái)就只會(huì)請(qǐng)求這個(gè)路由所需要的資源,并且通常依賴(lài)也都比較扁平。餓了么移動(dòng)站的大部分腳本依賴(lài)都是普通的 <script> 元素,因此他們可以在文檔解析早期就被瀏覽器的preloader掃描出來(lái)并且開(kāi)始請(qǐng)求,其效果其實(shí)與顯式的是一致的,見(jiàn)圖1所示。

圖片發(fā)自簡(jiǎn)書(shū)App

圖1 有無(wú) <link rel=“preload”> 的效果對(duì)比

? ? ? 我們還將所有關(guān)鍵的靜態(tài)資源都伺服在同一域名下(不再做域名散列),以更好地利用HTTP2帶來(lái)的多路復(fù)用(Multiplexing)。同時(shí),我們也在進(jìn)行著對(duì)API進(jìn)行Server Push的實(shí)驗(yàn)。

? ? ? Render,渲染初始路由,盡快讓?xiě)?yīng)用可被交互

? ? ? 既然所有初始路由的依賴(lài)都已經(jīng)就緒,我們就可以盡快開(kāi)始初始路由的渲染,這有助于提升應(yīng)用諸如首次渲染時(shí)間、可交互時(shí)間等指標(biāo)。多頁(yè)應(yīng)用并不使用基于JavaScript的路由,而是傳統(tǒng)的HTML跳轉(zhuǎn)機(jī)制,所以對(duì)于這一部分,多頁(yè)應(yīng)用其實(shí)不用額外做什么。

? ? ? Precache,用Service Worker預(yù)緩存剩下的路由

? ? ? ? 這一部分就需要Service Worker的參與了。Service Worker是一個(gè)位于瀏覽器與網(wǎng)絡(luò)之間的客戶(hù)端代理,它已可攔截、處理、響應(yīng)流經(jīng)的HTTP請(qǐng)求,使得開(kāi)發(fā)者得以從緩存中向Web應(yīng)用提供資源而聞名。不過(guò), Service Worker其實(shí)也可以主動(dòng)發(fā)起 HTTP 請(qǐng)求,在“后臺(tái)”預(yù)請(qǐng)求與預(yù)緩存我們未來(lái)所需要的資源,見(jiàn)圖2所示。

圖片發(fā)自簡(jiǎn)書(shū)App

圖2 Service Worker預(yù)緩存未來(lái)所需要的資源

? ? ? 我們已經(jīng)使用Webpack在構(gòu)建過(guò)程中進(jìn)行.vue編譯、文件名哈希等工作,于是我們編寫(xiě)了一個(gè)Webpack插件來(lái)幫助收集需要緩存的依賴(lài)到一個(gè)“預(yù)緩存清單”中,并使用這個(gè)清單在每次構(gòu)建時(shí)生成新的Service Worker文件。在新的Service Worker被激活時(shí),清單里的資源就會(huì)被請(qǐng)求與緩存,這其實(shí)與SW-Precache 這個(gè)庫(kù)的運(yùn)行機(jī)制非常接近。

? ? ? 實(shí)際上,我們只對(duì)標(biāo)記為“關(guān)鍵路由”的路由進(jìn)行依賴(lài)收集。你可以將這些“關(guān)鍵路由”的依賴(lài)?yán)斫鉃槲覀冋麄€(gè)應(yīng)用的"App Shell" 或者說(shuō)“安裝包”。一旦它們都被緩存,或者說(shuō)成功安裝,無(wú)論用戶(hù)是在線離線,我們的Web應(yīng)用都可以從緩存中直接啟動(dòng)。對(duì)于那些并不那么重要的路由,我們則采取在運(yùn)行時(shí)增量緩存的方式。我們使用的SW-Toolbox提供了LRU替換策略與TTL失效機(jī)制,可以保證我們的應(yīng)用不會(huì)超過(guò)瀏覽器的緩存配額。

? ? ? Lazy-Load,按需懶加載、懶實(shí)例化剩下的路由

? ? ? 懶加載與懶實(shí)例化剩下的路由對(duì)于SPA是一件相對(duì)麻煩點(diǎn)兒的事情,你需要實(shí)現(xiàn)基于路由的code splitting與異步加載。幸運(yùn)的是,這又是一件不需要多頁(yè)應(yīng)用擔(dān)心的事情,多頁(yè)應(yīng)用中的各個(gè)路由天生就是分離的。

? ? ? 值得說(shuō)明的是,無(wú)論單頁(yè)還是多頁(yè)應(yīng)用,如果在上一步中,我們已經(jīng)將這些路由的資源都預(yù)先下載與緩存好了,那么懶加載就幾乎是瞬時(shí)完成的了,這時(shí)候我們就只需要付出實(shí)例化的代價(jià)。

? ? 至此,我們對(duì)PRPL的四部分含義做了詳細(xì)說(shuō)明。有趣的是,我們發(fā)現(xiàn)多頁(yè)應(yīng)用在實(shí)現(xiàn)PRPL這件事甚至比單頁(yè)還要容易一些。那么結(jié)果如何呢?

? ? ? 根據(jù)Google推出的Web性能分析工具Lighthouse(v1.6),在模擬的3G網(wǎng)絡(luò)下,用戶(hù)的初次訪問(wèn)(無(wú)任何緩存)大約在2秒左右達(dá)到“可交互”,可以說(shuō)非常不錯(cuò),見(jiàn)圖3所示。而對(duì)于再次訪問(wèn),由于所有資源都直接來(lái)自于Service Worker緩存,頁(yè)面可以在1秒左右就達(dá)到可交互的狀態(tài)了。

圖片發(fā)自簡(jiǎn)書(shū)App

圖3 Lighthouse跑分結(jié)果

? ? ? 但是,故事并不是這么簡(jiǎn)單得就結(jié)束了。在實(shí)際體驗(yàn)中我們發(fā)現(xiàn),應(yīng)用在頁(yè)與頁(yè)的切換時(shí),仍然存在著非常明顯的白屏空隙,見(jiàn)圖4所示。由于PWA是全屏運(yùn)行,白屏對(duì)用戶(hù)體驗(yàn)所帶來(lái)的負(fù)面影響甚至比以往在瀏覽器內(nèi)更大。我們不是已經(jīng)用Service Worker緩存了所有資源了嗎,怎么還會(huì)這樣呢?

圖片發(fā)自簡(jiǎn)書(shū)App

圖4 從首頁(yè)點(diǎn)擊到發(fā)現(xiàn)頁(yè),跳轉(zhuǎn)過(guò)程中的白屏

? ? ? 多頁(yè)應(yīng)用的陷阱:重啟開(kāi)銷(xiāo)

? ? ? 與SPA不同,在多頁(yè)應(yīng)用中,路由的切換是原生的瀏覽器文檔跳轉(zhuǎn)(Navigating across documents),這意味著之前的頁(yè)面會(huì)被完全丟棄而瀏覽器需要為下一個(gè)路由的頁(yè)面重新執(zhí)行所有的啟動(dòng)步驟:重新下載資源、重新解析HTML、重新運(yùn)行JavaScript、重新解碼圖片、重新布局頁(yè)面、重新繪制……即使其中的很多步驟本是可以在多個(gè)路由之間復(fù)用的。這些工作無(wú)疑將產(chǎn)生巨大的計(jì)算開(kāi)銷(xiāo),也因此需要付出相當(dāng)多的時(shí)間成本。

? ? ? ? 圖5中為我們的入口頁(yè)(同時(shí)也是最重要的頁(yè)面)在兩倍CPU節(jié)流模擬下的Profile數(shù)據(jù)。即使我們可以將“可交互時(shí)間”控制在 1 秒左右,我們的用戶(hù)仍然會(huì)覺(jué)得這對(duì)于“僅僅切換個(gè)標(biāo)簽”來(lái)說(shuō)實(shí)在是太慢了。

圖片發(fā)自簡(jiǎn)書(shū)App

圖5 入口頁(yè)在兩倍CPU節(jié)流模擬下的Profile數(shù)據(jù)

? ? ? 巨大的JavaScript重啟開(kāi)銷(xiāo)

? ? ? 根據(jù)Profile,我們發(fā)現(xiàn)在首次渲染(First Paint)發(fā)生之前,大量的時(shí)間(900ms)都消耗在了JavaScript的運(yùn)行上(Evaluate Script)。幾乎所有腳本都是阻塞的(Parser-blocking),不過(guò)因?yàn)樗械腢I都是由JavaScript/Vue.js驅(qū)動(dòng)的,倒也不會(huì)有性能影響。這900ms中,約一半是消耗在Vue.js運(yùn)行時(shí)、組件、庫(kù)等依賴(lài)的運(yùn)行上,而另一半則花在了業(yè)務(wù)組件實(shí)例化時(shí)Vue.js的啟動(dòng)與渲染上。從軟件工程角度來(lái)說(shuō),我們需要這些抽象,所以這里并不是想責(zé)怪JavaScript或是Vue.js所帶來(lái)的開(kāi)銷(xiāo)。

? ? ? 但是,在SPA中, JavaScript的啟動(dòng)成本是均攤到整個(gè)生命周期的:每個(gè)腳本都只需要被解析與編譯一次,諸如生成Virtual DOM等較重的任務(wù)可以只執(zhí)行一次,像Vue.js的ViewModel或是Virtual DOM這樣的大對(duì)象也可以被留在內(nèi)存里復(fù)用??上г诙囗?yè)應(yīng)用里就不是這樣了,我們每次切換頁(yè)面都為JavaScript付出了巨大的重啟代價(jià)。

瀏覽器的緩存啊,能不能幫幫忙?

能,也不能。

? ? ? V8提供了代碼緩存(code caching),可以將編譯后的機(jī)器碼在本地拷貝一份,這樣我們就可以在下次請(qǐng)求同一個(gè)腳本時(shí)一次省略掉請(qǐng)求、解析、編譯的所有工作。而且,對(duì)于緩存在Service Worker配套的? ? ? ? ? Cache Storage中的腳本,會(huì)在第一次執(zhí)行后就觸發(fā)V8的代碼緩存,這對(duì)于我們的多頁(yè)切換能提供不少幫助。

? ? ? 另外一個(gè)你或許聽(tīng)過(guò)的瀏覽器緩存叫做“進(jìn)退緩存”, Back-Forward Cache,簡(jiǎn)稱(chēng)bfcache。瀏覽器廠商對(duì)其的命名各異, Opera稱(chēng)之為Fast History Navigation, Webkit稱(chēng)其為Page Cache。但是思路都一樣,就是我們可以讓瀏覽器在跳轉(zhuǎn)時(shí)把前一頁(yè)留存在內(nèi)存中,保留JavaScript與DOM的狀態(tài),而不是全都銷(xiāo)毀掉。你可以隨便找個(gè)傳統(tǒng)的多頁(yè)網(wǎng)站在iOS Safari上試試,無(wú)論是通過(guò)瀏覽器的前 進(jìn)后退按鈕、手勢(shì),還是通過(guò)超鏈接(會(huì)有一些不同),基本都可以看到瞬間加載的效果。

? ? ? Bfcache其實(shí)非常適合多頁(yè)應(yīng)用。但不幸的是,Chrome由于內(nèi)存開(kāi)銷(xiāo)與其多進(jìn)程架構(gòu)等原因目前并不支持。 Chrome現(xiàn)階段僅僅只是用了傳統(tǒng)的HTTP磁盤(pán)緩存,來(lái)稍稍簡(jiǎn)化了一下加載過(guò)程而已。對(duì)于Chromium內(nèi)核霸占的Android生態(tài)來(lái)說(shuō),我們沒(méi)法指望了。

為“感知體驗(yàn)”奮斗

? ? ? 盡管多頁(yè)應(yīng)用面臨著現(xiàn)實(shí)中的不少性能問(wèn)題,我們并不想這么快就妥協(xié)。一方面,我們嘗試盡可能減少在頁(yè)面達(dá)到可交互時(shí)間前的代碼執(zhí)行量,比如減少/推遲一些依賴(lài)腳本的執(zhí)行,還有減少初次渲染的DOM節(jié)點(diǎn)數(shù)以節(jié)省Virtual DOM的初始化開(kāi)銷(xiāo)。另一方面,我們也意識(shí)到應(yīng)用在感知體驗(yàn)上還有更多的優(yōu)化空間。

Chrome產(chǎn)品經(jīng)理Owen寫(xiě)過(guò)一篇? ? ? ? Reactive Web Design: The secret to building web apps that feel amazing,談到兩種改進(jìn)感知體驗(yàn)的手段:一是使用骨架屏(Skeleton Screen)來(lái)實(shí)現(xiàn)瞬間加載;二是預(yù)先定義好元素的尺寸來(lái)保證加載的穩(wěn)定。跟我們的做法可以說(shuō)不謀而合。

? ? ? 為了消除白屏?xí)r間,我們同樣引入了尺寸穩(wěn)定的骨架屏來(lái)幫助我們實(shí)現(xiàn)瞬間的加載與占位。即使是在硬件很弱的設(shè)備上,我們也可以在點(diǎn)擊切換標(biāo)簽后立刻渲染出目標(biāo)路由的骨架屏,以保證UI是穩(wěn)定、連續(xù)、有響應(yīng)的。我錄了兩個(gè)視頻放在Youtube上,不過(guò)如果你是國(guó)內(nèi)讀者,你可以直接訪問(wèn)餓了么移動(dòng)網(wǎng)站來(lái)體驗(yàn)實(shí)地的效果。最終效果如圖6所示。

圖片發(fā)自簡(jiǎn)書(shū)App

圖6 在添加骨架屏后,從發(fā)現(xiàn)頁(yè)點(diǎn)回首頁(yè)的效果

? ? ? 這效果本該很輕松的就能實(shí)現(xiàn),不過(guò)實(shí)際上我們還費(fèi)了點(diǎn)功夫。

在構(gòu)建時(shí)使用 Vue 預(yù)渲染骨架屏

? ? ? 你可能已經(jīng)想到了,為了讓骨架屏可以被Service Worker緩存,瞬間加載并獨(dú)立于JavaScript渲染,我們需要把組成骨架屏的HTML標(biāo)簽、 CSS樣式與圖片資源一并內(nèi)聯(lián)至各個(gè)路由的靜態(tài)*.html文件中。

? ? ? 不過(guò),我們并不準(zhǔn)備手動(dòng)編寫(xiě)這些骨架屏。你想啊,如果每次真實(shí)組件有迭代(每一個(gè)路由對(duì)我們來(lái)說(shuō)都是一個(gè)Vue.js組件),我們都需要手動(dòng)去同步每一個(gè)變化到骨架屏的話,那實(shí)在是太繁瑣且難以維護(hù)了。好在,骨架屏不過(guò)是當(dāng)數(shù)據(jù)還未加載進(jìn)來(lái)前,頁(yè)面的一個(gè)空白版本而已。如果我們能將骨架屏實(shí)現(xiàn)為真實(shí)組件的一個(gè)特殊狀態(tài)——“空狀態(tài)”的話,從理論上就可以從真實(shí)組件中直接渲染出骨架屏來(lái)。

? ? ? 而Vue.js的多才多藝就在這時(shí)體現(xiàn)出來(lái)了,我們真的可以用Vue.js 的服務(wù)端渲染模塊來(lái)實(shí)現(xiàn)這個(gè)想法,不過(guò)不是用在真正的服務(wù)器上,而是在構(gòu)建時(shí)用它把組件的空狀態(tài)預(yù)先渲染成字符串并注入到HTML模板中。你需要調(diào)整Vue.js組件代碼使得它可以在Node上執(zhí)行,有些頁(yè)面對(duì)DOM/BOM的依賴(lài)一時(shí)無(wú)法輕易去除得,我們目前只好額外編寫(xiě)一個(gè)*.shell.vue來(lái)暫時(shí)繞過(guò)這個(gè)問(wèn)題。

關(guān)于瀏覽器的繪制(Painting)

? ? ? ? HTML文件中有標(biāo)簽并不意味著這些標(biāo)簽就能立刻被繪制到屏幕上,你必須保證頁(yè)面的關(guān)鍵渲染路徑是為此優(yōu)化的。很多開(kāi)發(fā)者相信將Script標(biāo)簽放在body的底部就足以保證內(nèi)容能在腳本執(zhí)行之前被繪制,這對(duì)于能渲染不完整DOM樹(shù)的瀏覽器(比如桌面瀏覽器常見(jiàn)的流式渲染)來(lái)說(shuō)可能是成立的。但移動(dòng)端的瀏覽器很可能因?yàn)榭紤]到較慢的硬件、電量消耗等因素并不這么做。不僅如此,即使你曾被告知設(shè)為async或defer的腳本就不會(huì)阻塞HTML解析了,但這可不意味著瀏覽 器就一定會(huì)在執(zhí)行它們之前進(jìn)行渲染。

? ? ? 首先我想澄清的是,根據(jù) HTML 規(guī)范 Scripting 章節(jié), async腳本是在其請(qǐng)求完成后立刻運(yùn)行的,因此它本來(lái)就可能阻塞到解析。只有defer(且非內(nèi)聯(lián))與最新的type=module被指定為“一定不會(huì)阻塞解析”(不過(guò)defer目前也有點(diǎn)小問(wèn)題……我們稍后會(huì)再提到),見(jiàn)圖7所示。

圖片發(fā)自簡(jiǎn)書(shū)App

圖7 具有不同屬性的Script腳本對(duì)HTML解析的阻塞情況

? ? ? 而更重要的是,一個(gè)不阻塞HTML解析的腳本仍然可能阻塞到繪制。我做了一個(gè)簡(jiǎn)化的“最小多頁(yè)P(yáng)WA”(Minimal Multi-page PWA,或MMPWA)來(lái)測(cè)試這個(gè)問(wèn)題:我們?cè)谝粋€(gè)async(且確實(shí)不阻塞HTML解析)腳本中,生成并渲染1000個(gè)列表項(xiàng),然后測(cè)試骨架屏能否在腳本執(zhí)行之前渲染出來(lái)。圖8是通過(guò)USB Debugging在我的Nexus 5真機(jī)上錄制的Profile。

圖片發(fā)自簡(jiǎn)書(shū)App

圖8 通過(guò)USB Debugging在Nexus 5真機(jī)上錄制的Profile

? ? ? 是的,出乎意料嗎?首次渲染確實(shí)被阻塞到腳本執(zhí)行結(jié)束后才發(fā)生。究其原因,如果我們?cè)跒g覽器還未完成上一次繪制工作之前就過(guò)快得進(jìn)行了DOM操作,我們親愛(ài)的瀏覽器就只好拋棄所有它已經(jīng)完成的像素,且一直要等待到DOM操作引起的所有工作結(jié)束之后才能重新進(jìn)行下一次渲染。而這種情況更容易在擁有較慢CPU/GPU的移動(dòng)設(shè)備上出現(xiàn)。

黑魔法:利用setTimeout()讓繪制提前

? ? ? 不難發(fā)現(xiàn),骨架屏的繪制與腳本執(zhí)行實(shí)際是一個(gè)競(jìng)態(tài)。大概是Vue.js太快了,我們的骨架屏還是有非常大的概率繪制不出來(lái)。于是我們想著如何能讓腳本執(zhí)行慢點(diǎn),或者說(shuō),“懶”點(diǎn)。于是我們想到了一個(gè)經(jīng)典的Hack: setTimeout(callback, 0)。我們?cè)囍袽MPWA中的DOM操作(渲染1000個(gè)列表)放進(jìn)setTimeout(callback, 0)里……

? ? ? ? 當(dāng)當(dāng)!首次渲染瞬間就被提前了,見(jiàn)圖9所示。如果你熟悉瀏覽器的事件循環(huán)模型(Event Loop)的話,這招Hack其實(shí)是通過(guò)setTimeout的回調(diào)把DOM操作放到了事件循環(huán)的任務(wù)隊(duì)列中以避免它在當(dāng)前循環(huán)執(zhí)行,這樣瀏覽器就得以在主線程空閑時(shí)喘息一下(更新一下渲染)了。如果你想親手試試 MMPWA的話,你可以訪問(wèn)github.com/Huxpro/mmpwa 或 huangxuan.me/mmpwa/ ,查看代碼與Demo。我把UI設(shè)計(jì)成了A/B Test的形式并改為渲染5000個(gè)列表項(xiàng)來(lái)讓效果 更夸張一些。

圖片發(fā)自簡(jiǎn)書(shū)App

圖9 利用Hack技術(shù),提前完成骨架屏的繪制

? ? ? ? 回到餓了么PWA上,我們同樣試著把new Vue()放到了setTimeout中。果然,黑魔法再次顯靈,骨架屏在每次跳轉(zhuǎn)后都能立刻被渲染。這時(shí)的Profile看起來(lái)是這樣的,見(jiàn)圖10所示。

圖片發(fā)自簡(jiǎn)書(shū)App

圖10 為感知體驗(yàn)進(jìn)行各種優(yōu)化后的最終Profile

? ? ? ? 現(xiàn)在,我們?cè)?00ms時(shí)觸發(fā)首次渲染(骨架屏),在600ms時(shí)完成真實(shí)UI的渲染并達(dá)到頁(yè)面的可交互。你可以詳細(xì)對(duì)比下圖9和圖10所示的優(yōu)化前后Profile的區(qū)別。

被我“defer”的有關(guān)defer的Bug

? ? ? ? 不知道你發(fā)現(xiàn)沒(méi)有,在圖10的Profile中,我們?nèi)匀挥胁簧倌_本是阻塞了HTML解析的。好吧,讓我解釋一下,由于歷史原因,我們確實(shí)保留了一部分的阻塞腳本,比如侵入性很強(qiáng)的lib-flexible,我們沒(méi)法輕易去除它。不過(guò), Profile里的大部分阻塞腳本實(shí)際上都設(shè)置了defer,我們本以為他們應(yīng)該在HTML解析完成之后才被執(zhí)行,結(jié)果被Profile打了一臉。

? ? ? 我和Jake Archibald 聊了一下,果然這是Chrome的Bug: defer的腳本被完全緩存時(shí),并沒(méi)有遵守規(guī)范等待解析結(jié)束,反而阻塞了解析與渲染。Jake已經(jīng)提交在crbug上了,一起給它投票吧。

? ? ? ? 最后,圖11是優(yōu)化后的Lighthouse跑分結(jié)果,同樣可以看到明顯的性能提升。需要說(shuō)明的是,能影響Lighthouse跑分的因素有很多,所以我建議你以控制變量(跑分用的設(shè)備、跑分時(shí)的網(wǎng)絡(luò)環(huán)境等)的方式來(lái)進(jìn)行對(duì)照實(shí)驗(yàn)。

圖片發(fā)自簡(jiǎn)書(shū)App

圖11 優(yōu)化后的Lighthouse跑分結(jié)果

? ? ? 最后為大家展示下應(yīng)用的架構(gòu)示意圖,見(jiàn)圖12所示。

圖片發(fā)自簡(jiǎn)書(shū)App

圖12 應(yīng)用架構(gòu)示意圖

一些感想

? ? ? 多頁(yè)應(yīng)用仍然有很長(zhǎng)的路要走

? ? ? Web是一個(gè)極其多樣化的平臺(tái)。從靜態(tài)的博客,到電商網(wǎng)站,再到桌面級(jí)的生產(chǎn)力軟件,它們?nèi)际荳eb這個(gè)大家庭的第一公民。而我們組織Web應(yīng)用的方式,也同樣只會(huì)更多而不會(huì)更少:多頁(yè)、單頁(yè)、 Universal JavaScript應(yīng)用、 WebGL,以及可以預(yù)見(jiàn)的Web Assembly。不同的技術(shù)之間沒(méi)有貴賤,但是適用場(chǎng)景的差距確是客觀存在的。

? ? ? Jake 曾在 Chrome Dev Summit 2016 上說(shuō)過(guò)“PWA!== SPA”??墒潜M管我們已經(jīng)用上了一系列最新的技術(shù)(PRPL、 Service Worker、 App Shell……),我們?nèi)匀灰驗(yàn)槎囗?yè)應(yīng)用模型本身的缺陷有著難以逾越的一些障礙。多頁(yè)應(yīng)用在未來(lái)可能會(huì)? ? ? 有“bfcache API”、 Navigation Transition等新的規(guī)范以縮小跟SPA的距離,不過(guò)我們也必須承認(rèn),時(shí)至今日,多頁(yè)應(yīng)用的局限性也是非常明顯的。

? ? ? 而PWA終將帶領(lǐng)Web應(yīng)用進(jìn)入新的時(shí)代

? ? ? 即使我們的多頁(yè)應(yīng)用在升級(jí)PWA的路上不如單頁(yè)應(yīng)用來(lái)得那么閃亮,但是PWA背后的想法與技術(shù)卻實(shí)實(shí)在在地幫助我們?cè)赪eb平臺(tái)上提供了更好的用戶(hù)體驗(yàn)。

? ?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,040評(píng)論 25 709
  • 喝酵素的十大作用 ?維持血液的弱堿性; ?抗炎殺菌作用; ?血液凈化作用; ?增強(qiáng)免疫力; ?修復(fù)細(xì)胞作用; ?分...
    青島王建成閱讀 633評(píng)論 0 0
  • 時(shí)間管理是有效的運(yùn)用時(shí)間,降低變動(dòng)性。通過(guò)事先的規(guī)劃來(lái)提升自己的工作效率。在此簡(jiǎn)單分享一些關(guān)于時(shí)間管理的觀點(diǎn)。 1...
    ppmoon閱讀 1,031評(píng)論 0 49
  • 對(duì)于一個(gè)要出校園舞臺(tái)的大學(xué)生來(lái)說(shuō),有什么比聽(tīng)前輩的經(jīng)驗(yàn)更重要的呢,不管是經(jīng)歷,還是教訓(xùn)
    取個(gè)靜靜回家吧閱讀 115評(píng)論 1 0

友情鏈接更多精彩內(nèi)容