京東三級(jí)列表頁持續(xù)架構(gòu)優(yōu)化—前端優(yōu)化實(shí)踐
在持續(xù)開發(fā)一個(gè)核心系統(tǒng)過程中,除了滿足業(yè)務(wù)需求外,還應(yīng)該考慮系統(tǒng)未來的架構(gòu),追求***的系統(tǒng)的可用性、高性能和穩(wěn)定性。這個(gè)過程是一個(gè)長(zhǎng)期積累和重構(gòu)的過程。
每個(gè)應(yīng)用都要滿足自己特定的需求,因?yàn)樯虡I(yè)條件、應(yīng)用場(chǎng)景、用戶期望,以及功能復(fù)雜性各不相同。盡管如此,如果應(yīng)用必須對(duì)用戶作出響應(yīng),那我們就必須從用戶角度來考慮可感知的處理時(shí)間這個(gè)常量。事實(shí)上,雖然生活節(jié)奏越來越快——至少我們感覺如此,但人類的感知和反應(yīng)時(shí)間則一直都沒有變過:
這個(gè)表格解釋了Web 性能社區(qū)總結(jié)的經(jīng)驗(yàn)法則:必須250 ms 內(nèi)渲染頁面,或者至少提供視覺反饋,才能保證用戶不走開。如果想讓人感覺很快,就必須在幾百 ms 內(nèi)響應(yīng)用戶操作。超過1s,用戶的預(yù)期流程就會(huì)中斷,心思就會(huì)向其他任務(wù)轉(zhuǎn)移,而超過10s,除非你有反饋,否則用戶基本上就會(huì)終止任務(wù)!
下面將從前端、服務(wù)器端、緩存、兜底等來說說如何優(yōu)化京東三級(jí)列表頁。
前端優(yōu)化
京東三級(jí)列表頁從優(yōu)化到上線,已經(jīng)經(jīng)歷了兩個(gè)618和一個(gè)雙11的考驗(yàn),每天有上億的訪問量,頁面打開時(shí)間在20-80毫秒(在某些地區(qū)或低帶寬下會(huì)大于100ms)。
優(yōu)化四原則
- 精簡(jiǎn)和瘦身頁面,首屏優(yōu)先展示出來;
- 需用戶交互的部分惰性加載;
- 能不執(zhí)行的先別執(zhí)行,惰性執(zhí)行;
- 滾屏惰性加載。
一、HTML文檔要精簡(jiǎn)
目的:盡快渲染出頁面并達(dá)到可交互的狀態(tài)。
方法:
1、如果非必須,盡量只生成首屏需要的html數(shù)據(jù);
2、優(yōu)先獲取資源、提前解析。如首屏需要的css和js;如果不考慮維護(hù)成本,可以把首屏需要的css和js放到文檔中;
3、發(fā)現(xiàn)和優(yōu)先安排關(guān)鍵網(wǎng)絡(luò)資源,盡早分派請(qǐng)求并取得頁面;
4、文檔精簡(jiǎn)后,服務(wù)端生成程序耗時(shí)短,性能才會(huì)好。
如列表頁的頭、面包屑、品牌區(qū)、屬性篩選區(qū)、60個(gè)商品主圖數(shù)據(jù),這些是服務(wù)端模板渲染輸出;而剩余部分是在前端JS惰性加載或生成。
二、需用戶交互的部分惰性加載
對(duì)于三級(jí)列表頁品牌區(qū),服務(wù)端只渲染18個(gè)品牌,用戶在點(diǎn)更多時(shí),ajax異步加載其他的。對(duì)于整個(gè)屬性是篩選區(qū)服務(wù)端只渲染5行,其他行用戶在點(diǎn)更多時(shí),js從文檔嵌入資源中取到數(shù)據(jù),并渲染成html。這樣做可以保證服務(wù)端計(jì)算少,提升服務(wù)端性能,減少數(shù)據(jù)傳輸。如下圖點(diǎn)“更多”時(shí)才加載更多的品牌,因?yàn)橛行┤?jí)類目有非常多品牌,如果不采用這種方式,整個(gè)頁面渲染非常慢。
因?yàn)樾枰猄EO的原因,京東三級(jí)列表頁不能使用bigpipe等技術(shù)來進(jìn)行更優(yōu)的處理。
三、能不執(zhí)行的先別執(zhí)行,惰性執(zhí)行
上圖是三級(jí)列表頁最重要的商品區(qū)(商品主圖+N個(gè)關(guān)聯(lián)商品小圖),每個(gè)商品的區(qū)域都是完全一樣的;如果在服務(wù)端拼裝整個(gè)商品區(qū)域的話,尤其涉及到小圖部分,會(huì)有非常多的重復(fù)html元素;我們把體驗(yàn)和減少頁面內(nèi)容進(jìn)行了折中處理;服務(wù)端渲染輸出商品主圖部分;小圖部分通過json數(shù)據(jù)嵌入到頁面,然后通過js惰性執(zhí)行渲染。這樣可以很好地對(duì)頁面進(jìn)行瘦身。而且小圖資源是頁面嵌入的,非異步加載;沒有網(wǎng)絡(luò)請(qǐng)求,用戶基本感知不到異步帶來的渲染閃動(dòng)問題。下圖就是頁面嵌入的小圖json數(shù)據(jù)。
四、滾屏惰性加載
三級(jí)列表頁的60個(gè)商品區(qū)域的圖片和頁尾都是當(dāng)用戶向下滾動(dòng)頁面時(shí),才去加載當(dāng)前屏幕中的圖片和模塊。這樣可以節(jié)省服務(wù)器帶寬和壓力,提升頁面整體渲染時(shí)間。
上邊就介紹完了三級(jí)列表頁在優(yōu)化時(shí)使用的最主要的四個(gè)原則,而實(shí)際優(yōu)化過程中,還涉及到非常多的優(yōu)化細(xì)節(jié),如下部分將介紹這些細(xì)節(jié)。
將一些JS/CSS資源直接嵌入頁面
把資源嵌入文檔可以減少請(qǐng)求的次數(shù)。比如頁面需要的js 、css數(shù)據(jù)。如下圖所示:
上圖中的這些js對(duì)象,是后端渲染輸出的,因此不適合放入單獨(dú)的js文件,直接在頁面中嵌入輸出會(huì)更好些。slaveWareList是小圖的列表對(duì)象。如果放在服務(wù)端模板渲染輸出的話,首先需要進(jìn)行一些循環(huán)拼裝頁面;另外會(huì)使頁面體積變得非常大。權(quán)衡之后決定放到前端js渲染輸出。這樣也帶來了一些好處:減輕服務(wù)端壓力,提升渲染模板性能和減少服務(wù)端執(zhí)行時(shí)間;服務(wù)端不用生成html,文檔減少上百個(gè)div,減少頁面大小和網(wǎng)絡(luò)開銷;提前放到文檔中,不用異步調(diào)用;用戶基本感知不到渲染過程。
對(duì)引入的資源排定優(yōu)先次序
根據(jù)自己系統(tǒng)的業(yè)務(wù),對(duì)每種資源定優(yōu)先級(jí):對(duì)必需的資源優(yōu)先加載,而低優(yōu)先級(jí)的請(qǐng)求保存在隊(duì)列中延時(shí)加載或等待必需資源加載完再加載;如:搜索推薦熱詞、頂部三個(gè)熱賣商品接口、60個(gè)主商品的圖片、價(jià)格優(yōu)先加載。而對(duì)于庫存、促銷信息、廣告詞、預(yù)售商品、店鋪信息等,延后加載。對(duì)于點(diǎn)擊流,廣告統(tǒng)計(jì)數(shù)據(jù)則延時(shí)兩秒再加載。
應(yīng)用js緩存來存儲(chǔ)公有屬性和商品信息屬性
三級(jí)列表頁中的每個(gè)商品都是一個(gè)對(duì)象,存放在一個(gè)map中,通過ajax接口異步填充和維護(hù)商品的屬性。用于后續(xù)用戶交互用。同時(shí)維護(hù)成本也會(huì)降低;即頁面中用到的每個(gè)商品數(shù)據(jù)放入一個(gè)map中,如果沒有則異步加載;如果有直接使用;即這些數(shù)據(jù)是公共數(shù)據(jù)。
Ajax接口***調(diào)用
頁面往往依賴很多的異步接口,因此要對(duì)異步接口進(jìn)行壓測(cè),找出接口的***調(diào)用方式。如京東三級(jí)列表頁依賴價(jià)格、庫存、廣告詞、店鋪信息等異步調(diào)用接口。而頁面有時(shí)候會(huì)出現(xiàn)多達(dá)300多個(gè)商品,如果用一個(gè)get請(qǐng)求把這些sku做參數(shù),性能非常慢,那么就要采用分組分批調(diào)用。如頁面商品在300個(gè)時(shí),價(jià)格接口分六組,***組30個(gè),第二組30個(gè),第三組60個(gè),第四組60個(gè),第五組100個(gè),第六組100個(gè)。
DNS預(yù)解析
對(duì)可能的域名進(jìn)行提前解析,避免將來HTTP請(qǐng)求時(shí)的DNS延遲。如對(duì)價(jià)格、庫存、圖片、單品頁等服務(wù)預(yù)解析。
減少HTTP重定向
HTTP 重定向極費(fèi)時(shí)間,特別是不同域名之間的重定向,更加費(fèi)時(shí);這里面既有額外的DNS 查詢、TCP 握手,還有其他延遲。***的重定向次數(shù)為零。比如三級(jí)列表頁以前是http://list.jd.com/12-12-12.html,而現(xiàn)在是http://list.jd.com/list.html?cat=12,12,12;在過渡期間可以重定向,但是過渡完成后就沒必要重定向了。
使用CDN(內(nèi)容分發(fā)網(wǎng)絡(luò))
把數(shù)據(jù)放到離用戶地理位置更近的地方,可以顯著減少每次TCP連接的網(wǎng)絡(luò)延遲,增大吞吐量。比如京東三級(jí)列表頁、商品詳情頁、公共JS、CSS。
傳輸壓縮過的內(nèi)容(Gzip壓縮)
傳輸前應(yīng)該壓縮應(yīng)用資源,把要傳輸?shù)淖止?jié)減至最少:確保對(duì)每種要傳輸?shù)馁Y源采用***的壓縮手段。所有文本資源都應(yīng)該使用Gzip壓縮,然后再在客戶端與服務(wù)端間傳輸。一般來說,Gzip可以減少60%~80%的文件大小,也是一個(gè)相對(duì)簡(jiǎn)單(只要在服務(wù)器上配置一個(gè)選項(xiàng)),但優(yōu)化效果較好的舉措。(對(duì)于壓縮級(jí)別,經(jīng)過不同服務(wù)器多次壓測(cè),建議Nginx設(shè)置為1-4)
去掉不必要的資源
任何請(qǐng)求都不如沒有請(qǐng)求快,把一些非必須的或者可異步的,或者可延遲的盡量延遲請(qǐng)求。
在客戶端緩存資源
應(yīng)該緩存應(yīng)用資源,從而避免每次請(qǐng)求都發(fā)送相同的內(nèi)容。
無狀態(tài)域名
Cookie 在很多應(yīng)用中都是常見的性能瓶頸,很多開發(fā)者都會(huì)忽略它給每次請(qǐng)求增加的額外負(fù)擔(dān);減少請(qǐng)求的HTTP首部數(shù)據(jù)(比如HTTP cookie),節(jié)省的時(shí)間相當(dāng)于幾次往返的延遲時(shí)間。如列表頁依賴的價(jià)格、庫存接口,采用3.cn無狀態(tài)域名,從而減少主域下cookie傳輸。
并行處理請(qǐng)求和響應(yīng)
請(qǐng)求和響應(yīng)的排隊(duì)都會(huì)導(dǎo)致延遲,無論是客戶端還是服務(wù)器端。這一點(diǎn)經(jīng)常被忽視,但卻會(huì)無謂地導(dǎo)致很長(zhǎng)延遲。
域名分區(qū)
當(dāng)頁面中非常多請(qǐng)求都是一個(gè)域名下資源時(shí),由于瀏覽器同時(shí)只能打開6個(gè)連接池,而且每個(gè)鏈接池是對(duì)不同域名起作用,所以很多請(qǐng)求一個(gè)域名會(huì)出現(xiàn)排隊(duì)現(xiàn)象。如果把這些請(qǐng)求域名分區(qū),讓請(qǐng)求并行,從而加快資源下載。如:頁面需要下載上百張圖片,對(duì)圖片進(jìn)行域名分區(qū)調(diào)用。京東大部分頁面都對(duì)圖片進(jìn)行了域名分區(qū)調(diào)用:
http://img10.360buyimg.com/
http://img11.360buyimg.com/
http://img12.360buyimg.com/
http://img13.360buyimg.com/
http://img14.360buyimg.com/
拼合和連接
合并鏈接:把多個(gè)JavaScript 或CSS 文件組合為一個(gè)文件。拼合:把多張圖片組合為一個(gè)更大的復(fù)合的圖片(CSS Sprites)。
服務(wù)端寫相關(guān)信息到header
把服務(wù)器IP后兩位寫到header,如果有問題,方便定位哪臺(tái)服務(wù)器。ups:后端路由的所有服務(wù)器都取到。把緩存***信息或異常走兜底了,把后端運(yùn)行狀態(tài)寫到header。Head-status:***、未***、異常等狀態(tài)。
服務(wù)端架構(gòu)
Nginx+Lua(OpenResty)+golang+redis緩存計(jì)算,后續(xù)再把列表頁的架構(gòu)整理出來。
作者:王向維,京東商城三級(jí)列表頁架構(gòu)師,完成列表頁的nodejs版本到nginx+lua版本的變遷,并做了大量三級(jí)列表頁的服務(wù)端和前端的優(yōu)化工作。
【本文來自51CTO專欄作者張開濤的微信公眾號(hào)(開濤的博客),公眾號(hào)id: kaitao-1234567】