上周我在斯達哥爾摩住了幾天,出席了 http 研讨會,參與了不少吸引人的讨論。其中一次是關于 http 推送及其優缺點、早期實驗結果的。
由于早期實驗部署結果不那麼理想,人們對 http 推送大體持着懷疑态度,不過我想分享下自己更樂觀一些的觀點。
http 推送能做哪些預加載不能做的事?
從懷疑者那裡一再聽到的觀點是,“推送相對于預加載來說,隻不過節省了一次 rtt(round trip time)而已”。在實踐中,這并非總是對的,有一個使用案例,推送可以完成,但預加載無法做到。
利用伺服器思考時間think-time
如今,html 響應很少隻是單純的靜态資源了。它們通常都是通過資料庫擷取所需的資訊、使用進階語言(可能略微慢一些)動态生成的。雖然後端響應時間确實可以而且應當優化,但幾百毫秒的響應時間也并不常見。
有一個常見的建議,“提早 flush” html,在查詢資料庫并建構動态内容的同時,發送 html 的首個 chunk 塊。但是,并非所有服務端的構架都能這麼簡單地實作。
另外一個讓問題變得困難的因素是,需要開始向浏覽器發送資料時,我們尚無法确定響應的建構是否會完全成功。為避免出現響應建立邏輯出錯(比如說,資料庫錯誤或者服務端代碼運作失敗),我們需要在應用邏輯中建立一種“復原”已發送響應資料的方式,并向使用者展示錯誤資訊。
盡管這肯定有可能做得到(甚至是自動化的),但目前還沒有一種通用的方式能夠作為協定的一部分。
是以一般場景是,web 伺服器等待後端數百毫秒以建構頁面,而後開始傳回資料。這時候我們就碰到了慢啟動(譯者注:slow start,可參考此文),是以首次 rtt 隻能發送大約 14 kb 資料,第二次 28 kb 左右,如此等等。由此我們知道,将 html 發送出去,需要用去伺服器思考時間加上慢啟動時間。在思考時間期間,浏覽器對接下來所需的資源一無所知,故也不會針對接下來所需資源的關鍵路徑發送任何請求。
而且,即使我們試着耍小聰明,針對那些資源添加 preload 報頭,若我們不提早 flush 文檔開頭,那還是沒有對思考時間加以利用。
現在,将這個與使用 http 推送能做的事情做個對比。伺服器可以利用思考時間來推送相關的關鍵性資源 —— 尤其是 css 和 js。這樣一來,當思考時間結束時,我們極有可能已将所有關鍵性資源都推送給浏覽器了。
還有額外好處,這些資源也預熱了 tcp 連接配接,也提升了擁塞視窗congestion window,確定思考時間之後的首個 rtt 中,可以使用 28 kb,56 kb,乃至更大的擁塞視窗發送 html(這取決于思考時間的長短,以及在此期間我們推送了多少資源)。
一起來看下具體案例:一個 120 kb 的 html 頁面,關鍵 css 有 24kb,關鍵 js 有 74 kb,在 100ms rtt、無限帶寬的網絡環境下是如何加載的?
沒有 http 推送的情況下,生成 html 等待了 300ms,接着 4 次 rtt 發送 html,因為慢啟動的緣故,使用了一個 rtt 請求 js 和 css。在首次渲染之前,時間超過了 800 毫秒。
無推送情況下的頁面加載
使用 http 推送,css 和 js 會在 html 請求到達之後盡快推送出去,發送它們需要 3 個 rtt(又一次因為慢啟動),它們将擁塞視窗增大至 128 kb 左右,将要發送 html 時,一個 rtt 就能可以了。首次渲染總時間: 400 毫秒。
http 推送情況下的頁面加載
首次渲染加速了 50%!也不算很差嘛。。。
http 推送不盡如意的地方
我認為人們在錯誤地使用 http 推送的原因之一是,他們在某些并不能提供任何好處甚至損害效率的場景下使用它。
盲目推送靜态資源
使用 http 推送可能做的錯事之一就是告訴你自己,“啊,這些資源是所有頁面都需要的,把它們配置成所有頁面都推送”。
這很糟糕,原因是緩存。在通路第一個頁面之後,這些資源很可能就在使用者的浏覽器緩存中,然而你卻在悶頭推送。你可能會争辯說,這可比内聯所有這些資源好多了。是這樣的,不錯,但,我必須反過來告訴你,内聯資源也是糟糕的主意。
是以,若你在以這種方式盲目推送資源,請確定它是你想要内聯在頁面中的唯一的資源,也就是關鍵的 css。否則,你就是在冒險讓重複的請求變慢。
你可能會以為,流重置stream resets會幫助推送已緩存的資源去避免浪費帶寬和時間。你可能錯了。很顯然,并非所有浏覽器會檢查緩存并終止已緩存資源的推送。就算它們會這樣,在流重置信号到達伺服器之前,還是使用了一整個 rtt 時間發送資料。尤其是有多個資源時,這樣做将可能帶來大量資料浪費。
将内容放入浏覽器緩存
你可能以為,推送會将資源放入浏覽器緩存,可以用來做一些像使目前資源失效這樣的工作。至少目前不是如此。研讨會上的讨論的話題之一就是現實問題,可能我們需要改變目前的推送行為,支援與浏覽器緩存直接互動。不過目前,推送還做不到這些。推送響應進入推送緩存,隻有真實請求它們時才會放到 http 緩存中。
是以,如果你在推送資源,希望它們在未來的某個頁面中使用,那麼浏覽器有可能在用到它們之前已經将它們扔出推送緩存之外了。
至少目前的實作是這樣的。
填補 html 下發之後的管道
通常,在頁面的下載下傳循環中,使用的帶寬之間會存在間隙。這意味着我們沒能盡快下發資源,通常這是因為浏覽器發現資源的延遲。
盡管我們應當盡量下發頁面所需資源以填滿這些間隙,但通常使用預加載比推送更好。預加載将緩存、cookie以及内容協商納入考慮,它不會像推送那樣存在着過度發送或錯誤發送的風險。就填補這些間隙而言,推送并無任何優勢,所有的隻是劣勢。故最好不要使用推送達成此目的,使用預加載吧。
緩存摘要cache digests
從上面我們可以看到,http 推送的一大缺點就是,伺服器并不必然清楚浏覽器的緩存狀态,是以在推送時我們可能會将已在緩存中存在的資源推送出去。
有一個标準擴充的提案,叫做 cache digests。其基本思想是浏覽器在 http/2 連接配接初始化之後,向伺服器發送摘要,伺服器在下發資源之前能夠精确判斷資源是否已在浏覽器緩存中存在。
該提案尚處于早期,可能需要簡化,這樣實作起來花費更少,不過我敢說,離開這個特性,http 推送隻能算半成品。
總結
http 推送可以用來顯著提升加載性能。正确使用時能為首個關鍵路徑加載提速,帶來性能名額的改善。
推送依然是非常新的技術,像其他所有新工具一樣,在找到使用的最優方式之前,還有很長的路要走。這一路多少會有點痛苦。
是故早期實驗的初始結果,可能并非全如我們所希望的那樣。讓我們把那些結果作為标志,訓示我們關于推送的使用需要更多聰明才智吧,别妄下結論說它是無用的特性