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