京東PLUS會員項(xiàng)目是國內(nèi)第一個電商付費(fèi)會員項(xiàng)目瑟曲,正式開通的會員數(shù)量已破千萬综膀。我團(tuán)隊(duì)從2016年接手這個項(xiàng)目的前端開發(fā)工作歉秫,一路見證了它的高速成長籽暇,也為此貢獻(xiàn)了自己的力量温治。
這個項(xiàng)目有幾個特點(diǎn):
第一,需求多戒悠。移動端使用 H5 開發(fā)罐盔,曾有人問為什么不用原生或者 RN 開發(fā)? 我覺得吧,以這個項(xiàng)目的需求數(shù)量和迭代速度來看救崔,連 H5 都難以 hold 的住惶看,還是不要奢望原生和 RN 了。
第二六孵,產(chǎn)品經(jīng)理多纬黎。一般的項(xiàng)目對接一兩個產(chǎn)品經(jīng)理,這個項(xiàng)目我們需要對接一個異地的產(chǎn)品經(jīng)理“團(tuán)隊(duì)”劫窒;一般的項(xiàng)目換產(chǎn)品經(jīng)理一個一個的換本今,這個項(xiàng)目一批一批的換……我們已經(jīng)送走好幾屆PLUS會員產(chǎn)品經(jīng)理了。鐵打的研發(fā)主巍,流水的產(chǎn)品經(jīng)理冠息。
所以說,PLUS會員項(xiàng)目是業(yè)務(wù)方滴孕索,也是項(xiàng)目經(jīng)理滴逛艰,還是產(chǎn)品經(jīng)理滴,但終歸是俺們研發(fā)滴搞旭。每念及此散怖,我的耳邊總會響起葉倩文的那首老歌:“天地悠悠菇绵,過客匆匆,潮起又潮落...”镇眷。
書歸正傳酌予。用戶眾多和需求迭代頻繁音同,確保線上安全穩(wěn)定始終是第一要務(wù)港谊。所以在架構(gòu)調(diào)整和性能優(yōu)化方面我們一直都小心翼翼贾漏,以一些小修小補(bǔ)為主,只有到大規(guī)模改版的時候才會有大的升級改造具伍。不過翅雏,平時我們對這些問題的思考和實(shí)踐卻不曾停止過,我們驗(yàn)證了一些行之有效的優(yōu)化方案沿猜,在下一波改版中將會得到應(yīng)用枚荣。
我雖然不完全認(rèn)同“前端開發(fā)每十八個月難度翻一倍”的說法碗脊,但這一行發(fā)展迭代速度快卻是不爭的事實(shí)啼肩。若等到這些優(yōu)化方案全都應(yīng)用上再出來念叨,可能就顯得不那么新鮮了衙伶。所以祈坠,我決定先把這些方案拿出來分享,和感興趣的小伙伴一起討論矢劲,進(jìn)一步完善赦拘。
這些方案主要針對移動端,優(yōu)化核心方向是提高首頁的加載速度芬沉,特別是首屏和弱網(wǎng)絡(luò)環(huán)境下的加載速度躺同。從持久化緩存、削減代碼量丸逸、優(yōu)化接口請求蹋艺、提升主觀感受等方面下手,比較大的改動是應(yīng)用 PWA
和升級架構(gòu)黄刚。 PWA
離線緩存可以極大的提升用戶體驗(yàn)捎谨,不過它對于首次加載速度并無提升作用,還得靠其他優(yōu)化手段憔维,這是一套組合拳涛救。我們先從架構(gòu)升級說起吧。
一业扒、架構(gòu)升級
項(xiàng)目計(jì)劃遷移到 Gaea4.0
腳手架[1]检吆,這是我們團(tuán)隊(duì)基于 webpack 4 開發(fā)的一套通用 Vue 單頁面應(yīng)用腳手架,此前的系列版本已經(jīng)過數(shù)十個項(xiàng)目的驗(yàn)證程储,還是比較穩(wěn)定的咧栗。近期新推出4.0版相較之前版本有著不小的改進(jìn)逆甜。
webpack 升級到了 4.0
Babel 升級到了 7.0
Vue-loader 升級到了 15
重構(gòu)了上傳插件,一鍵上傳到測試服務(wù)器更快更穩(wěn)定
針對我廠手機(jī)和電腦位于不同局域網(wǎng)無法互訪的問題致板,集成了自主研發(fā)的 Carefree 解決方案[2]交煞,方便真機(jī)測試調(diào)試
集成了 NutUI 組件庫[3],可按需加載需要的UI組件
集成了自主研發(fā)的基于swagger的數(shù)據(jù)mock工具SMOCK[4]
支持自動生成骨架屏[5]
支持 PWA
…
遷移有幾個主要目的:
首先斟或,實(shí)現(xiàn)本項(xiàng)目的 webpack 構(gòu)建工具升級到 4.0素征,之前是基于 webpack 2.0 開發(fā)的,webpack4 有不少提升萝挤,比如:
Scope Hoisting(作用域提升御毅,webpack3加入),通過減少閉包函數(shù)數(shù)量加快JS的執(zhí)行速度
生產(chǎn)環(huán)境構(gòu)建體積更小
開發(fā)環(huán)境通過優(yōu)化的增量構(gòu)建機(jī)制提升構(gòu)建速度怜珍,同時提供詳細(xì)的錯誤和提示
其次端蛆, Gaea4.0
的 Babel 是 7.0 版的,基于 Babel7 可以實(shí)現(xiàn)更智能的 Babel polyfill 按需加載酥泛。
再次今豆,本次優(yōu)化計(jì)劃嘗試的PWA、骨架屏等方案柔袁, Gaea4.0
都可以給予基礎(chǔ)支持呆躲。
最后, Gaea4.0
集成的Carefree捶索、新的上傳插件等功能將給未來的開發(fā)和真機(jī)調(diào)試帶來方便插掂。
二、Babel polyfill的按需加載
如今的 web 應(yīng)用開發(fā)都是在本地進(jìn)行構(gòu)建腥例,所以有條件在構(gòu)建階段把高版本的 JS 代碼編譯成低版本語法辅甥,這樣既使用了新語法,又解決了低版本瀏覽器的兼容問題燎竖。承擔(dān)這種轉(zhuǎn)換工作的最知名的工具當(dāng)屬 Babel 了璃弄。而一直以來,Babel 有個飽受詬病的地方底瓣,那就是 polyfill 問題谢揪。
Babel 默認(rèn)只轉(zhuǎn)換 JavaScript 語法,而不轉(zhuǎn)換新的 API捐凭,比如 Promise拨扶、Generator、Set茁肠、Maps患民、Symbol 等全局對象,一些定義在全局對象上的方法(比如 Object.assign)也不會被轉(zhuǎn)碼垦梆。如果想讓未轉(zhuǎn)碼的 API 可在低版本環(huán)境正常運(yùn)行匹颤,這就需要使用 polyfill仅孩。
polyfill 有多種方案,各有各的問題印蓖。目前應(yīng)用中通常使用 babel-polyfill 方案辽慕,而第三方庫中通常使用 babel-runtime 和 babel-plugin-transform-runtime 方案。
babel-polyfill 提供完整的環(huán)境墊片赦肃,包含所有 API 的降級模塊溅蛉,可以為新的 API 和全局對象上的方法提供兜底,其主要缺點(diǎn)是文件較大他宛,壓縮后大概八九十KB船侧。目前項(xiàng)目中采用這種方案,這次考慮予以優(yōu)化厅各,減少加載的代碼體積镜撩。
如上文提到,這一波改造會把項(xiàng)目遷移到 Gaea4.0
腳手架中队塘,新腳手架的 Babel 已經(jīng)升級到了最新的 7.0 版袁梗。Babel7 是 Babel6 推出近三年之后發(fā)布的一個斷崖式升級的大版本,包含很多新特性人灼,其中一個引人關(guān)注的特性就是支持更智能的按需加載 polyfill围段。
Babel7 主要是通過其提供的 @babel/preset-env
實(shí)現(xiàn)按需加載的顾翼。
使用 @babel/preset-env
也需要首先安裝 @babel/polyfill
投放,但最終打出的包并不會導(dǎo)入全部 polyfill。
npm install @babel/polyfill --save
同時适贸,需要在 .browserslistrc 文件或者 .babelrc 的 targets 字段中指定需要兼容的瀏覽器范圍灸芳。
之后在.babelrc文件中對 @babel/preset-env
進(jìn)行配置。
@babel/preset-env
與按需加載 polyfill 相關(guān)的選項(xiàng)是 useBuiltIns
拜姿,它有兩個值需要重點(diǎn)關(guān)注: entry
和 usage
烙样。
當(dāng)值為 entry
時,Babel 會將 import"@babel/polyfill"
或者 require("@babel/polyfill")
語句根據(jù)我們指定的環(huán)境配置替換為單個的 polyfill require蕊肥。
如將
import "@babel/polyfill";
替換為
import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es7.string.pad-end";
當(dāng)值為 usage
時谒获,更加智能。Babel 會根據(jù)每個文件的需要和指定的環(huán)境配置添加特定的 polyfill壁却,更排×的是一個 bundle 中相同的 polyfill 只會加載一次,這也有助于減小 bundle 的體積展东。推測 Babel 是通過對文件進(jìn)行靜態(tài)分析實(shí)現(xiàn)的這種精準(zhǔn)的按需加載 polyfill 功能赔硫。
如
var a = new Promise();
轉(zhuǎn)換后(如果指定的環(huán)境不支持)
import "core-js/modules/es6.promise";
var a = new Promise();
轉(zhuǎn)換后(如果指定的環(huán)境支持)
var a = new Promise();
我們嘗試了一下,先指定需要兼容的瀏覽器范圍盐肃,然后安裝 @babel/polyfill
并將 @babel/preset-env
的 useBuiltIns
選項(xiàng)值設(shè)為 usage
爪膊。這樣 Babel 就會自動分析每一個文件并在考慮我們指定的瀏覽器兼容范圍的情況下权悟,為每個文件加載其需要的 polyfill。最終項(xiàng)目里只引入了部分 polyfill推盛,經(jīng)測算峦阁,打包后的代碼(min)較直接引入完整 babel-polyfill 的方案小60多KB,同時還避免了全局變量污染耘成。
在 Babel 的配置中開啟 Debug 模式拇派,構(gòu)建的時候可以看到每個文件中添加了哪些 polyfill:
(有從知乎遠(yuǎn)道而來的杠精問到:“這都什么年代了,還在兼容Android 4.0和iOS 8.0凿跳?”我嘆口氣件豌、聳聳肩,與該杠精握握手…)
關(guān)于這個問題的進(jìn)一步思考:
這種加載 polyfill 的方式已經(jīng)比傳統(tǒng)方式先進(jìn)了很多控嗜,但還是不完美茧彤,比如按照我們指定的瀏覽器范圍需要引入的某個 polyfill,對于高版本瀏覽器來說可能還是多余疆栏。
個人覺得一種比較理想的方案是先在編譯階段通過靜態(tài)分析確定可能需要 polyfill 的 API 范圍但并不打包 polyfill 進(jìn)去曾掂,而是當(dāng)用戶在瀏覽器中訪問這個頁面時,通過植入頁面的JS腳本逐一檢測當(dāng)前瀏覽器是否支持這些新的 API壁顶,把不支持的找出來珠洗,通過一個請求去服務(wù)端加載對應(yīng)的 polyfill 文件。當(dāng)然這需要類似 polyfill.io
的服務(wù)端 polyfill 方案支持若专。未來我們會沿著這個方向繼續(xù)探索许蓖。
三、持久化緩存
PWA
是真的火了调衰,現(xiàn)在的項(xiàng)目里沒用 PWA
出門都不好意思跟人打招呼膊爪。 PWA
的一系列功能中最重磅的非離線緩存莫屬了,雖說 H5 之前就有離線緩存(application cache)API嚎莉,可惜不好用米酬, PWA
離線緩存足以把它拍死在沙灘上。
從業(yè)務(wù)角度來講趋箩,我們認(rèn)為本項(xiàng)目不太適合離線訪問赃额,但我們可以利用 PWA
把靜態(tài)資源進(jìn)行離線緩存,提高頁面訪問速度叫确。
在這種場景下跳芳,用 ServiceWorker
不緩存頁面自身 HTML 和接口數(shù)據(jù),只緩存靜態(tài)資源启妹,且優(yōu)先使用緩存筛严。非首次訪問的情況下,靜態(tài)資源都會走緩存饶米,頁面訪問速度得以大幅提升桨啃。
但有一個問題车胡,就是頁面更新的問題。使用緩存優(yōu)先策略照瘾,意味著每次進(jìn)入頁面時匈棘,在有緩存的情況下直接使用緩存。如果緩存有更新析命,在緩存更新之后需要刷新頁面才能看到變化主卫。自動刷新頁面嚴(yán)重影響用戶體驗(yàn),而提示用戶去手動刷新鹃愤,在 APP 里看上去也有些奇怪簇搅,且不是所有有用戶都會去手動刷新的。對于PLUS會員這種需求排隊(duì)软吐,更新頻繁的項(xiàng)目瘩将,用戶感受到的影響可能會更多。HTML5 的離線緩存 API 也有這個問題凹耙,這當(dāng)然不是一個缺陷姿现,而是“優(yōu)先使用緩存”策略所決定的,只是不完全滿足我們的需求罷了肖抱。
針對這個問題备典,我們的解決方案是當(dāng)文件有更新時,同時修改緩存的版本號和頁面中引用這個文件的 URL 中的版本號意述,讓瀏覽器直接使用新文件提佣,不使用緩存。在頁面加載之后欲险,緩存也會更新镐依,下次訪問時匹涮,還會走緩存天试。
這個方案還有優(yōu)化空間,只有那些有變化的文件需要更改 URL 中的版本號然低,使用新文件喜每,而頁面中其他沒有發(fā)生變化的靜態(tài)資源還是可以也應(yīng)該繼續(xù)使用緩存。按照這個思路雳攘,我們應(yīng)把代碼中穩(wěn)定的带兜、不常變化的模塊(比如 Vue 及其插件)盡量提取出來,讓這部分內(nèi)容盡可能使用緩存吨灭,當(dāng)然必要的時候也可以通過相同的方式更新刚照。而經(jīng)常發(fā)生變化的部分(如業(yè)務(wù)代碼)應(yīng)獨(dú)立打包,體積越小越好喧兄,以減小頁面和緩存更新時的開銷无畔。
對于這些穩(wěn)定公共模塊的提取我們使用 webpack 內(nèi)置的 DllPlugin
和 DllReferencePlugin
插件來實(shí)現(xiàn)啊楚,通過這兩個插件提前對這些公共模塊進(jìn)行獨(dú)立編譯,打出一個 vendor.dll.js 的包浑彰,之后在這部分代碼沒有改動的情況下不再對它們進(jìn)行編譯恭理,所以項(xiàng)目平時的構(gòu)建速度也會提升不少。vendor.dll.js 包獨(dú)立存在郭变,hash 不會發(fā)生變化颜价,特別適合持久化緩存。
于是诉濒,我們的業(yè)務(wù)代碼有變化時周伦,只需要以新版號發(fā)布業(yè)務(wù)包(app.js)即可,vendor.dll.js 依然使用本地緩存未荒。
我們來看一下具體的加載情況横辆。
首次訪問,沒有 PWA
緩存茄猫,所有資源都走線上狈蚤。頁面加載之后,PWA會緩存靜態(tài)資源划纽。
之后的訪問脆侮,靜態(tài)資源優(yōu)先從緩存加載,速度極快勇劣。
當(dāng)業(yè)務(wù)代碼有更新時靖避,更改頁面中引用 app.js 文件的 URL 中的版本號,使得 app.js 不使用緩存比默,已緩存的其他靜態(tài)資源依然可以使用緩存幻捏。同時更改緩存的版本號,緩存也會在頁面加載之后更新命咐,新的 app.js 文件也會被緩存篡九。
再次訪問時,包括 app.js 在內(nèi)的靜態(tài)資源依然全部走緩存醋奠。
四榛臼、請求優(yōu)化
這個是一個前后端分離的項(xiàng)目,前端是標(biāo)準(zhǔn)的 Vue SPA窜司,完全通過接口同后端進(jìn)行數(shù)據(jù)交互沛善。PLUS會員業(yè)務(wù)邏輯本身比較復(fù)雜,涉及很多種用戶狀態(tài)塞祈,頁面邏輯也復(fù)雜金刁。不同用戶看到的界面不完全相同,這受用戶狀態(tài)和后臺配置等多種因素影響。
部分接口存在相互依賴的關(guān)系尤蛮,比如有接口要求傳用戶狀態(tài)漠秋,因此需要先行通過用戶信息接口拿到用戶狀態(tài)。再比如商品數(shù)據(jù)接口抵屿,需要先請求樓層配置信息接口庆锦,確定當(dāng)前頁面有哪些樓層,繼而才能決定去請求哪些樓層的數(shù)據(jù)轧葛。
這種串行的接口請求拖慢了首屏的渲染搂抒,這是目前影響首頁性能的一個主要問題,也是這次優(yōu)化的一個重點(diǎn)尿扯。
服務(wù)端渲染(如Vue SSR)求晶,首屏直出當(dāng)然是最理想的方案。但目前看來并不現(xiàn)實(shí)衷笋,這個項(xiàng)目的研發(fā)團(tuán)隊(duì)情況也比較復(fù)雜芳杏,前后端是兩個跨職場、跨部門的團(tuán)隊(duì)辟宗,且需求巨多爵赵,頁面改動頻繁。完全的前后端分離更有助于明確職責(zé)泊脐,提高效率空幻,減少扯皮。
另一個折中的方案是容客,在頁面上直接引一個后端的模板文件秕铛,后端研發(fā)同事通過這個模板文件把用戶狀態(tài)、樓層配置等前置信息打到頁面上缩挑,頁面在瀏覽器中初始化的時候直接讀取這些信息但两,然后再去請求那些依賴這些數(shù)據(jù)的接口。這樣即可避免串行請求的問題供置,同時還減少了幾個請求谨湘,有助于提高頁面加載和渲染速度。這次優(yōu)化士袄,我們計(jì)劃采用這種方案悲关。
優(yōu)化后,關(guān)鍵請求大幅提前娄柳,頁面開始渲染的時間明顯提前:
夢想還是要有的。前后端分離是一種進(jìn)步艘绍,但徹底的分離赤拒,也不盡善盡美,比如會有首屏加載速度和 SEO 方面的困擾。 前后端分離+服務(wù)端首屏渲染
看起來是個更優(yōu)的方案挎挖,它結(jié)合了前后端分離和服務(wù)端渲染兩者的優(yōu)點(diǎn)这敬,既做到了前后端分離,又能保證首頁渲染速度蕉朵,還有利于 SEO崔涂。但在 Vue、React 等前端框架大行其道的今天始衅,服務(wù)端渲染早已不是當(dāng)年套 HTML 頁面那么簡單了冷蚂,即便只渲染個首屏。前后端同構(gòu)可能是比較好的解決方案汛闸,而這種場景下服務(wù)端渲染工作顯然由前端來承擔(dān)更合適蝙茶,所以用 Node.js 搞個中間層是必要的。
五诸老、骨架屏
通過一系列優(yōu)化隆夯,除了客觀上首屏渲染時間的明顯縮短,我們還額外給頁面加上了骨架屏(skeleton screen)别伏,讓用戶主觀感受到的頁面加載和渲染速度比真實(shí)情況還快蹄衷。虛虛實(shí)實(shí),用兵之道也厘肮,一切為了用戶體驗(yàn)宦芦。
先來了解一下骨架屏的概念。骨架屏指的是在頁面數(shù)據(jù)加載完成前轴脐,先給用戶展示出的頁面大致結(jié)構(gòu)调卑,之后渲染出真實(shí)頁面內(nèi)容將其換掉。這是近兩年流行起來的加載控件大咱,本質(zhì)上是界面加載過程中的過渡效果恬涧。
在加載完成前把網(wǎng)頁的大概輪廓預(yù)先顯示,接著逐漸加載真正內(nèi)容碴巾,這樣既可緩解用戶等待的焦灼情緒溯捆,又能使界面的加載過程顯得更自然通暢,減少了長時間白屏或者閃爍厦瓢。骨架屏能給人一種頁面內(nèi)容“已經(jīng)渲染出一部分”的感覺提揍,相較于傳統(tǒng)的 loading 效果,體驗(yàn)更佳煮仇。
我們團(tuán)隊(duì)對骨架屏技術(shù)有比較深入的研究劳跃,開發(fā)過一個名為 @nutui/draw-page-structure
[4]的webpack插件,可實(shí)現(xiàn)通過 puppeteer 自動生成純 DOM 形式的頁面骨架屏浙垫,并支持自動插入到指定頁面刨仑。如果對自動生成的效果不滿意郑诺,還允許定制和調(diào)整。
我們用這個插件在項(xiàng)目里小試了一把杉武,效果還是不錯滴辙诞。純 DOM 形式的骨架屏代碼,比圖片轻抱、Canvas等形式數(shù)據(jù)量更小飞涂,調(diào)整起來也更靈活。
六祈搜、圖片格式
Plus會員頻道首頁是一個典型的電商頁面较店,包含大量的圖片。使用新興的圖片格式可以大大減少加載的圖片體積夭问,并有助于提升圖片的解析和渲染速度泽西,進(jìn)而提升頁面渲染速度。對于移動web來說缰趋,還有一個重要的優(yōu)點(diǎn)——節(jié)省用戶的流量(中國移動30M5塊錢呢捧杉,哈哈)。
去年我們在項(xiàng)目里應(yīng)用了 WebP
格式秘血,收效不錯味抖。比如某張背景圖片,壓縮后的 png 格式是35KB灰粮,而轉(zhuǎn)成 WebP
只有4KB仔涩,兩者基本看不出質(zhì)量上的差別。
新興圖片格式的應(yīng)用的主要障礙還是兼容性粘舟,以 WebP
為例熔脂,谷歌系的瀏覽器以及歐朋瀏覽器支持情況良好,F(xiàn)irefox柑肴、Edge 也都在新版本提供了支持霞揉,可惜蘋果公司一直沒有跟進(jìn),Safari 直到現(xiàn)在也沒有要支持的跡象晰骑,iOS 上的應(yīng)用如果想支持适秩,還需自行打包解析庫(經(jīng)測試發(fā)現(xiàn)iOS版的京東APP已經(jīng)提供了支持,點(diǎn)個贊)硕舆。
我們使用 WebP
的方式是在頁面上通過JS判斷當(dāng)前瀏覽器是否支持 WebP
秽荞,如果支持,則在 body 上增加一個名為 “webp” 的 class抚官,同時把判斷結(jié)果寫入 localStorage扬跋,之后再進(jìn)入頁面時直接從 localStorage 里讀取,不用每次都執(zhí)行判斷的代碼了耗式。然后在頁面的 css 中通過 “.webp” 選擇器胁住、在 Vue 的圖片過濾器中通過判斷結(jié)果來決定是否加載 WebP
格式圖片趁猴。
document.createElement('canvas').toDataURL('image/webp').indexOf('data:image/webp') === 0
這次的優(yōu)化刊咳,我們考慮增加對我廠 DPG
圖片格式的支持彪见。
DPG 是我廠基礎(chǔ)架構(gòu)部-智能存儲部推出圖片壓縮技術(shù),經(jīng)過 DPG 壓縮后的圖片兼容 jpeg娱挨,同時全平臺余指、全部瀏覽器都支持,DPG 是一種有損壓縮技術(shù)跷坝,但通過5名用戶10000張圖片的人眼瀏覽測試酵镜,和 WebP 的清晰度對比沒有差距。該技術(shù)可以有效地減少圖片大小50%柴钻,減少 CDN 帶寬流量 50%淮韭,加快圖片用戶在設(shè)備上的渲染速度。
基于我個人的理解贴届, DPG
格式應(yīng)該是對 jpeg 格式圖片通過一定算法進(jìn)行了二次壓縮靠粪,其本質(zhì)上還是 jpeg(雖然擴(kuò)展名改了),這也才能有所謂”全平臺瀏覽器支持“的可能性毫蚓。所以占键,特別適合將 jpeg 格式的圖片替換為 DPG
格式,當(dāng)然前提是服務(wù)器上有 DPG
格式圖片元潘。我廠的圖片系統(tǒng)會自動生成上傳圖片對應(yīng)的 DPG
格式圖片畔乙。所以我們定的 DPG
格式使用條件就是原圖是 jpeg 格式,且圖片位于我廠圖片系統(tǒng)中翩概。在兼顧既有的 WebP
格式圖片加載邏輯的基礎(chǔ)上牲距,我們梳理后的圖片加載邏輯如下圖所示:
原文:https://juejin.im/entry/5c6613ed518825629d075478?utm_source=gold_browser_extension