[譯]Google Chrome中的高性能網絡
【譯注】這部分不再詳細翻譯,只列出核心意思。
驅動Chrome繼續前進的核心原則包括:
-
Speed: 做最快的(fastest)的瀏覽器。
-
Security:為用戶提供最為安全的(most secure)的上網環境。
-
Stability: 提供一個健壯且穩定的(resilient and stable)的Web應用平臺。
-
Simplicity: 以簡練的用戶體驗(simple user experience)封裝精益求精的技術(sophisticated technology)。
本文關將注于第一點,速度。
關于性能的方方面面
一個現代瀏覽器就是一個和操作系統一樣的平臺。在Chrome之前的瀏覽器都是單進程的應用,所有頁面共享相同的地址空間和資源。引入多進程架構這是Chrome最為著名的改進【譯注:省略一些反復談論的細節】。
一個進程內,Web應用主要需要執行三個任務:獲取資源,頁面 排版及渲染,和運行JavaScript。渲染和腳本都是在運行中交替以單線程的方式運行的,其原因是為了保持DOM的一致性,而JavaScript本 身也是一個單線程的語言。所以優化渲染和腳本運行無論對于頁面開發者還是瀏覽器開發者都是極為重要的。
Chrome的渲染引擎是WebKit, JavaScript Engine則使用深入優論的V8 ( “V8″ JavaScript runtime )。 但是,如果網絡不暢,無論優化V8的JavaScript執行,還是優化WebKit的解析和渲染,作用其實很有限。巧婦難為無米之炊,數據沒來就得等著!
相對于用戶體驗,作用最為明顯的就是如何優化網絡資源的加載順 序、優先級及每一個資源的延遲時間(latency)。也許你察覺不到,Chrome網絡模塊每天都在進步,逐步降低每個資源的加載成本:向DNS lookups學習,記住頁面拓撲結構(topology of the web), 預先連接可能的目標網址,等等,還有很多。從外面來看就是一個簡單的資源加載的機制,但在內部卻是一個精彩的世界。
關于Web應用
開始正題前,還是先來了解一下現在網頁或者Web應用在網絡上的需求。
HTTP Archive 項目一直在追蹤網頁構建。除了頁面內容外,它還會分析流行頁面使用的資源數量,類型,頭信息以及不同目標地址的元數據(metadata)。下面是2013年1月的統計資料,由300,000目標頁面得出的平均數據:
-
1280 KB
-
包含88個資源(Images,JavaScript,CSS …)
-
連接15個以上的不同主機(distinct hosts)
這些數字在過去幾年中一直持續增長( steadily increasing ),沒有停下的跡象。這說明我們正不斷地建構一個更加龐大的、野心勃勃的網絡應用。還要注意,平均來看每個資源不過12KB, 表明絕大多數的網絡傳輸都是短促(short and bursty)的。這和TCP針對大數據、流式(streaming)下載的方向不一致,正因為如此,而引入了一些并發癥。下面就用一個例子來抽絲剝繭,一窺究竟……
一個Resource Request的一生
W3C的 Navigation Timing specification 定義了一組API,可以觀察到瀏覽器的每一個請求(request)的時序和性能數據。下面了解一些細節:
給定一個網頁資源地址后,瀏覽器就會檢查本地緩存和應用緩存。如果之前獲取過并且有相應的緩存信息( appropriate cache headers )(如Expires, Cache-Control, etc.), 就會用緩存數據填充這個請求,畢竟最快的請求就是沒有請求( the fastest request is a request not made )。否則,我們重新驗證資源,如果已經失效(expired),或者根本就沒見過,一個耗費網絡的請求就無法避免地發送了。
給定了一個主機名和資源路徑后,Chrome先是檢查現有已建立的連接(existing open connections)是否可以復用, 即sockets指定了以(scheme、host和port)定義的連接池(pool)。但如果配置了一個代理,或者指定了 proxy auto-config (PAC)腳本,Chrome就會檢查與proxy的連接。PAC腳本基于URL提供不同的代理,或者為此指定了特定 的規則。與每一個代理間都可以有自己的socket pool。最后,上述情況都不存在,這個請求就會從DNS查詢(DNS lookup)開始了,以便獲得它的IP地址。
幸運的話,這個主機名已經被緩存過。否則,必須先發起一個 DNS Query。這個過程所需的時間和ISP,頁面的知名度,主機名在中間緩存(intermediate caches)的可能性,以及authoritative servers的響應時間這些因素有關。也就是說這里變量很多,不過一般還不致于到幾百毫秒那么夸張。
拿到解析出的IP后,Chrome就會在目標地址間打開一個新TCP連接,我們就要執行一個3度握手( “three-way handshake” ): SYN > SYN-ACK > ACK。這個操作每個新的TCP連接都必須完成,沒有捷徑。根據遠近,路由路徑的選擇,這個過程可能要耗時幾百毫秒,甚至幾秒。而到現在,我們連一個有效的字節都還沒收到。
當TCP握手完成了,如果我們連接的是一個HTTPS地址,還有一個SSL握手過程,同時又要增加最多兩輪的延遲等待。如果SSL會話被緩存了,就只需一次。
最后,Chrome終于要發送HTTP請求了 (如上面圖示中的requestStart)。 服務器收到請求后,就會傳送響應數據(response data)回到客戶端。這里包含最少的往返延遲和服務的處理時間。然后一個請求就完成了。但是,如果是一個HTTP重定向(redirect)的話?我們 又要從頭開始這個過程。如果你的頁面里有些冗余的重定向,最好三思一下!
你得出所有的延遲時間了嗎? 我們假設一個典型的寬帶環境:沒有本地緩存,相對較快的DNS lookup(50ms), TCP握手,SSL協商,以及一個較快服務器響應時間(100ms)和一次延遲(80ms,在美國國內的平均值):
-
50ms for DNS
-
80ms for TCP handshake (one RTT)
-
160ms for SSL handshake (two RTT’s)
-
40ms (發送請求到服務器)
-
100ms (服務器處理)
-
40ms (服務器回傳響應數據)
一個請求花了470毫秒, 其中80%的時間被網絡延遲占去了。看到了吧,我們真得有很多事情要做!事實上,470毫秒已經很樂觀了:
-
如果服務器沒有達到到初始TCP的擁塞窗口( congestion window ),即4-15KB,就會引入更多的往返延遲。
-
SSL延遲也可能變得更糟。如果需要獲取一個沒有的認證(certificate)或者執行 online certificate status check (OCSP), 都會讓我們需要一個新的TCP連接,又增加了數百至上千毫秒的延遲。
怎樣才算”夠快”?
前面可以看到服務器響應時間僅是總延遲時間的20%,其它都被DNS,握手等操作占用了。過去用戶體驗研究( user experience research )表明用戶對延遲時間的不同反應:
-
0 - 100ms 迅速
-
100 - 300ms 有點慢
-
300 - 1000ms 機器還在運行
-
1s+ 想想別的事……
-
10s+ 我一會再來看看吧……
上表同樣適用于頁面的性能表現: 渲染頁面,至少要在250ms內給個回應來吸引住用戶。這就是簡單地針對速度。從Google, Amazon, Microsoft,以及其它數千個站點來看,額外的延遲直接影響頁面表現:流暢的頁面會吸引更多的瀏覽、以及更強的用戶吸引力(engagement) 和頁面轉換率(conversion rates).
現在我們知道了理想的延遲時間是250ms,而前面的示例告訴我們,DNS Lookup, TCP和SSL握手,以及request的準備時間花去了370ms, 即便不考慮服務器處理時間,我們也超出了50%。
對于絕大多數的用戶和網頁開發者來說,DNS, TCP,以及SSL延遲都是透明,很少有人會想到它。這也就是為什么Chrome的網絡模塊那么的復雜。
我們已經識別出了問題,下面讓我們深入一下實現的細節 …
深入Chrome的網絡模塊
多進程架構
Chrome的多進程架構為瀏覽器的網絡請求處理帶來了重要意義,它目前支持四種不同的執行模式( four different execution models )。
默認情況下,桌面的Chrome瀏覽器使用process-per-site模式, 將不同的網站頁面隔離起來, 相同網站的頁面組織在一起。舉個簡單的例子: 每個tab獨立一個進程。從網絡性能的角度上說,并沒什么本質上的不同,只是process-per- tabl模式更易于理解。
每一個tab有一個渲染進程(render process),其中包括了用于解析頁面(interpreting)和排版(layout out)的WebKit的排版引擎(layout engine), 即上圖中的HTML Render。還有V8引擎和兩者之間的DOM Bindings,如果你對這部分很好奇,可以看這里( great introduction to the plumbing )。
每一個這樣的渲染進程被運行在一個沙箱環境中,只會對用戶的電 腦環境做極有限的訪問-包括網絡。而使用這些資源,每一個渲染進程必須和瀏覽內核進程(browser[kernel] process)溝通,以管理每個渲染進程的安全性和訪問策略(access policies)。
進程間通訊(IPC)和多進程資源加載
渲染進程和內核進程之間的通訊是通過IPC完成的。在Linux和 Mac OS上,使用了一個提供異步命名管道通訊方式的socketpair()。每一個渲染進程的消息會被序列化地到一個專用的I/O線程中,然后再由它發到內 核進程。在接收端,內核進程提供一個過濾接口(filter interface)用于解析資源相關的IPC請求( ResourceMessageFilter ), 這部分就是網絡模塊負責的。
這樣做其中一個好處是所有的資源請求都由I/O進程處理,無論是UI產生的活動,或者網絡事件觸發的交互。在內核進程(browser/kernel process)的I/O線程解析資源請求消息,將轉發到一個 ResourceDispatcherHost 的單例(singleton)對象中處理。
這個單例接口允許瀏覽器控制每個渲染進程對網絡的訪問,也能達到有效和一致的資源共享:
-
Socket pool 和 connection limits: 瀏覽器可以限定每一個profile打開256個sockets, 每個proxy打開32個sockets, 而每一組{scheme, host, port}可以打開6個。注意同時針對一組{host,port}最多允計打開6個HTTP和6個HTTPS連接。
-
Socket reuse: 在Socket Pool中提供持久可用的TCP connections,以供復用。這樣可以為新的連接避免額外建立DNS、TCP和SSL(如果需要的話)所花費的時間。
-
Socket late-binding(延遲綁定): 網絡請求總是當Scoket準備好發送數據時才與一個TCP連接關連起來,所以首先有機會做到對請求有效分級(prioritization),比如,在 socket連接過程中可能會到達到一個更高優先級的請求。同時也可以有更好的吞吐率(throughput),比如,在連接打開過程中,去復用一個剛好 可用的socket, 就可以使用到一個完全可用的TCP連接。其實傳統的TCP pre-connect(預連接)及其它大量的優化方法也是這個效果。
-
Consistent session state(一致的會話狀態): 授權、cookies及緩存數據會在所有渲染進程間共享。
-
Global resource and network optimizations(全局資源和網絡優化): 瀏覽器能夠在所有渲染進程和未處理的請求間做更優的決策。比如給當前tab對應的請求以更好的優先級。
-
Predictive optimizations(預測優化): 通過監控網絡活動,Chrome會建立并持續改善預測模型來提升性能。
-
… 項目還在增加中。
單就一個渲染進程而言, 透過IPC發送資源請求很容易,只要告訴瀏覽器內核進程一個唯一ID, 后面就交給內核進程處理了。
跨平臺的資源加載
跨平臺也是Chrome網絡模塊的一個主要考量,包括Linux, Windows, OS X, Chrome OS, Android, 和iOS。 為此,網絡模塊盡量實現成了單進程模式(只分出了獨立的cache和proxy進程)的跨平臺函數庫, 這樣就可以在平臺間共用基礎組件(infrastructure)并分享相同的性能優化,更有機會做到同時為所有平臺進行優化。
相關的代碼可以在這里找到 the “src/net” subdirectory )。本文不會詳細展開每個組件,不過了解一下代碼結構可以幫助我們理解它的能力結構。 比如:
-
net/android 綁定到Android 運行時(runtime) [譯注(Horky):運行時真是一個很爛的術語,翻和沒翻一樣。]
-
net/base 公共的網絡工具函數。比如,主機解析, cookies, 網絡轉換偵測(network change detection),以及SSL認證管理
-
net/cookies 實現了Cookie的存儲、管理及獲取
-
net/disk_cache 磁盤和內存緩存的實現
-
net/dns 實現了一個異步的DNS解析器(DNS resolver)
-
net/http 實現了HTTP協議
-
net/proxy 代理(SOCKS 和 HTTP)配置、解析(resolution) 、腳本抓取(script fetching), …
-
net/socket TCP sockets,SSL streams和socket pools的跨平臺實現
-
net/spdy 實現了SPDY協議
-
net/url_request URLRequest, URLRequestContext和URLRequestJob的實現
-
net/websockets 實現了WebSockets協議
上面每一項都值得好好讀讀,代碼組織的很好,你還會發現大量的單元測試。
Mobile平臺上的架構和性能
移動瀏覽器正在大發展,Chrome團隊也視優化移動端的體驗為最高優先級。先要說明的是移動版的Chrome的并不是其桌面版本的直接移植,因為那樣根本不會帶來好的用戶體驗。移動端的先天特性就決定了它是一個資源嚴重受限的環境,在運行參數有一些基本的不同:
-
桌面用戶使用鼠標操作,可以有重疊的窗口,大的屏幕,也不用擔心電池。網絡也非常穩定,有大量的存儲空間和內存。
-
移動端的用戶則是觸摸和手勢操作,屏幕小,電池電量有限,通過只能用龜速且昂貴的網絡,存儲空間和內存也是相當受限。
再者,不但沒有典型的樣板移動設備,反而是有一大批各色硬件的設備。Chrome要做的,只能是設法兼容這些設備。好在Chrome有不同的運行模式(execution models),面對這些問題,游刃有余!
在Android版本上,Chrome同樣運用了桌面版本的多進程架構。一個瀏覽器內核進程,以及一個或多個渲染進程。但因為內存的限制,移動版的Chrome無法為每一個tabl運行一個特定的渲染進程,而是根據內存情況等條件決定一個最佳的渲染進程個數,然后就會在多個tab間共享這些渲染進程。
如果內存實在不足,或其它原因導致Chrome無法運行多進程,它就會切到單進程、多線程的模式。比如在iOS設備上,因為其沙箱機制的限制,Chrome只能運行在這種模式下。
關于網絡性能,首先Chrome在Android和iOS使用的是 各其它平臺相同的網絡模塊。這可以做到跨平臺的網絡優化,這也是Chrome明顯領先的優勢之一。所不同的是需要經常根據網絡情況和設備能力進行些調整, 包括推測優化(speculative optimization)的優先級、socket的超時設置和管理邏輯、緩存大小等。
比如,為了延長電池壽命,移動端的Chrome會傾向于延遲關閉空 閑的sockets (lazy closing of idle sockets), 通常是為了減少信號(radio)的使用而在打開新的socket時關閉舊的。另外因為預渲染(pre-rendering,稍后會介紹)會使用一定的網 絡和處理資源,它通常只在WiFi才會使用。
關于移動瀏覽體驗會獨立一章,也許就在POSA系列的下一期。
Chrome Predictor的預測功能優化
Chrome會隨著使用變得更快。它這個特性是通過一個單例對象Predictor來實現的。這個對象在瀏覽器內核進程(Browser Kernel Process)中實例化,它唯一的職責就是觀察和學習當前網絡活動方式,提前預估用戶下一步的操作。下面是一個示例:
-
用戶將鼠標停留在一個鏈接上,就預示著一個用戶的偏好以及下一步的瀏覽行為。這時Chrome就可以提前進行DNS Lookup及TCP握手。用戶的點擊操作平均需要將近200ms,在這個時間就可能處理完DNS和TCP相關的操作, 也就是省去幾百毫秒的延遲時間。
-
當在地址欄(Omnibox/URL bar) 觸發高可能性選項時,就同樣會觸發一個DNS lookup和TCP預連接(pre-connect),甚至在一個不可見的頁簽中進行預渲染(pre-render)!
-
我們每個人都一串天天會訪問的網站, Chrome會研究在這些頁面上的子資源, 并且嘗試進行預解析(pre-resolve), 甚至可能會進行預加載(pre-fetch)以優化瀏覽體驗。
除了上面三項,還有很多..
Chrome會在你使用過程中學習Web的拓撲結構,而不單單是你的瀏覽模式。理想的話,它將為你省去數百毫秒的延遲, 更接近于即時頁面加載的狀態. 正是為了這個目標,Chrome投入了以下的核心優化技術:
-
DNS預解析(pre-resolve):提前解析主機地址,以減少DNS延遲
-
TCP預連接(pre-connect):提前連接到目標服務器,以減少TCP握手延遲
-
資源預加載(prefetching):提前加載頁面的核心資源,以加載頁面顯示
-
頁面預渲染(prerendering):提前獲取整個頁面和相關子資源,這樣可以做到及時顯示
每一個決策都包含著一個或多個的優化, 用來克服大量的限制因素. 不過畢竟都只是預測性的優化策略,如果效果不理想,就會引入多余的處理和網絡傳輸。甚至可能會帶來一些加載時間上的負體驗。
Chrome如何處理這些問題呢? Predictor會盡量收集各種信息,諸如用戶操作,歷史瀏覽數據,以及來自渲染引擎(render)和網絡模塊自身的信息。
和Chrome中負責網絡事務調度的ResourceDispatcherHost不同,Predictor對象會針對用戶和網絡事務創建一組過濾器(filter):
-
IPC channel filter用來監控來自render進程的事務。
-
每個請求上都會加一個ConnectInterceptor 對象,這樣就可以跟蹤網絡傳輸的模式以及每一個請求的度量數據。
渲染進程(render process)會在一系列的事件下發送消息到瀏覽器進程(browser process), 這些事件被定義在一個枚舉(ResolutionMotivation)中以便于使用 ( url_info.h ):
enum ResolutionMotivation { MOUSE_OVER_MOTIVATED , // 鼠標懸停. OMNIBOX_MOTIVATED , // Omni-box建議進行解析. STARTUP_LIST_MOTIVATED , // 這是在前10個啟動項中的資源. EARLY_LOAD_MOTIVATED , // 有時需要使用prefetched來提前建立連接. // 下面定義了預加載評估的方式,會由一個navigation變量指定. // referring_url_也需要同時指定. STATIC_REFERAL_MOTIVATED , // 外部數據庫(External Database)建議進行解析。 LEARNED_REFERAL_MOTIVATED , // 前一次瀏覽(prior navigation建議進行解析. SELF_REFERAL_MOTIVATED , // 猜測下一個連接是不是需要進行解析. // <略> ... } ; |
通過這些給定的事 件,Predictor的目標就可以評估它成功的可能性, 然后再適時觸發操作。每一項事件都有其成功的機率、優先級以及時間戳,這些可以在內部維護一個用優先級管理的隊列,也是優化的一個手段。最終,對于這個隊 列中發出的每一個請求的成功率,都可以被Predictor追蹤到。基于這些數據,Predictor就可以進一步優化它的決策。
Chrome網絡架構小結
-
Chrome使用多進程架構,將渲染進程同瀏覽器進程隔離開來。
-
Chrome維護著一個資源分發器的實例(a single instance of the resource dispatcher), 它運行在瀏覽器內核進程,并在各個渲染進程間共享。
-
網絡層是跨平臺的,大部分情況下以單進程庫存在。
-
網絡層使用非阻塞式(no-blocking)操作來管理所有網絡任務。
-
共享的網絡層支持有效的資源排序、復用、并為瀏覽器提供在多進程間進行全局優化的能力。
-
每一個渲染進程通過IPC和資源分發器(resource dispatcher)通訊。
-
資源分發器(Resource dispatcher)通過自定義的IPC Filter解析資源請求。
-
Predictor在解析資源請求和響應網絡事務中學習,并對后續的網絡請求進行優化。
-
Predictor會根據學習到的網絡事務模式預測性的進行DNS解析, TCP握手,甚至是資源請求,為用戶實際操作時節省數百毫秒的時間。
了解晦澀的內部細節后,讓我們來看一下用戶可以感受到的優化。一切從全新的Chrome開始。
優化冷啟動(Cold-Boot)體驗
第一次啟動瀏覽器,它當然不可能了解你的使用習慣和喜歡的頁面。但事實上,我們大多數會在瀏覽器的冷啟動后做些類似的事情,比如到電子郵箱查看郵件,加一些新聞頁面、社交頁面及內部 頁面到我的最愛,諸如此類。這些頁面各有不同,但它們仍然具有一些相似性,所以Predictor仍然可以對這個過程提速。
Chrome記下了用戶在全新啟動瀏覽器時最常用的10個域名。當瀏覽器啟動時,Chrome會提前對這些域名進行DNS預解析。你可以在Chrome中使用chrome://dns查看到這個列表。在打開頁面的最上面的表格中會列出啟動時的備選域名列表。
通過Omnibox優化與用戶的交互
引入Omnibox是Chrome的一項創新, 并不是簡單地處理目標的URL。除了記錄之前訪問過的頁面URL,它還與搜索引擎的整合,并且支持在歷史記錄中進行全文搜索(比如,直接輸入頁面名稱)。
當用戶輸入時,Omnibox自動發起一個行為,要么查找瀏覽記錄中的URL, 要么進行一次搜索。每一次發起的操作都會被加以評分,以統計它的性能。你可以在Chrome輸入chrome://predictors來觀察這些數據。
Chrome維護著一個歷史記錄,內容包括用戶輸入的前置文字,采用的行為,以命中的資數。在上面的列表,你可以看到,當輸入g時,有76%的機會嘗試打開Gmail. 如果再補充一個m (就是gm), 打開Gmail的可能性增加到99.8%。
那么網絡模塊會做什么呢?上 表中的黃色和綠色對于ResourceDispatcher非常重要。如果有一個一般可能性的頁面(黃色), Chrome就是發起DNS預解析。如果有一個高可能性的頁面(綠色),Chrome還會在DNS解析后發起TCP預連接。如果這兩項都完成了,用戶仍然 繼續錄入,Chrome就會在一個隱藏的頁簽進行預渲染(pre-render)。
相對的,如果輸入的前置文字找不到合適的匹配項目,Chrome會向搜索引擎服務者發起DNS預解析和TCP預連,以獲取相似的搜索結果。
平均而言用戶從填寫查詢內容到評估給出的建議需要花費數百毫秒。此時Chrome可以在后臺進行預解析,預連接,甚至進行預渲染。再當用戶準備按下回車鍵時,大量的網絡延遲已經被提前處理掉了。
優化緩存性能
最快的請求就是沒有請求。 無論何時討論性能,都不能不談緩存。相信你已經為頁面上所有資源的都提供了Expires, ETag, Last-Modified和Cache-Control這些響應頭信息( response headers )。什么? 還沒有?那你還是先處理好再來看吧!
Chrome有兩種不同的內部緩存的實現:一種備份于本地磁盤(local disk),另一種則存儲于內存(in-memory)中。內存模式(in-memory)主要應用于無痕瀏覽模式( Incognito browsing mode ),并在關閉窗口清除掉。 兩種方式使用了相同的內部接口(disk_cache::Backend, 和disk_cache::Entry),大大簡化了系統架構。如果你想實現一個自己的緩存算法,可以很容易地實現進去。
在內部,磁盤緩存(disk cache)實現了它自己的一組數據結構, 它們被存儲在一個單獨的緩存目錄里。其中有索引文件(在瀏覽器啟動時加載到內存中),數據文件(存儲著實際數據,以及HTTP頭以及其它信息)。比較有趣 的是,16KB以下的文件存儲于共同的數據塊文件中(data block-files,即小文件集中存儲于一個大文件中),其它較大的文件才會存儲到自己專屬的文件中。最后,磁盤緩存的淘汰策略是維護一個LRU,通 過比如訪問頻率和資源的使用時間(age)的度量進行管理。
在Chrome開個頁簽,輸入chrome://net-internals/#httpCache。 如果你要看到實際的HTTP數據和緩存的響應處理,可以打開chrome://cache, 里面會列出所有緩存中可用的資源。打開每一項,還可以看到詳細的數據頭等信息。
優化DNS預連接
前面已經多次提到了DNS預解析,在深入實現之前,先匯總一下DNS預解析的場景和理由:
-
運行在渲染進程中的WebKit文檔解析器(document parser), 會為當前頁面上所有的鏈接提供一個主機名(hostname)列表,Chrome可以選擇是否提前解析。
-
當用戶要打開頁面時,渲染進程先會觸發一個鼠標懸停(hover)或按鍵(button down)事件。
-
Omnibox可能會針對一個高可能性的建議頁面發起解析請求。
-
Chrome Predictor會基于過往瀏覽記錄和資源請求數據發起主機解析請求。(下面會詳細解釋。)
-
頁面本身會顯式地要求Chrome對某些主機名稱進行預解析。
上述各項對于Chrome都只是一個線索。Chrome并不保證預解析一定會被執行,所有的線索會由Predictor進行評估,以決定后續的操作。最壞的情況下,可能無法及時解析主機名,用戶就必須等待一個 DNS解析時間,然后是TCP連接時間,最后是資源加載時間。Predictor會記下這個場景,在未來決策時相應地加以參考。總之,一定是越用越快。
之前提過到Chrome可以 記住每個頁面的拓撲(topology),并可以基于這個信息進行加速。還記得吧,平均每個頁面帶有88個資源,分別來自于30多個獨立的主機。每打開這 個頁面,Chrome會記下資源中比較常用的主機名,在后續的瀏覽過程中,Chrome就會發起針對某些主機或者全部主機的DNS解析,甚至是TCP預連接!
使用chrome://dns 就可以觀察到上面的數據(Google+頁面), 其中有6個子資源對應的主機名,并記錄了DNS預解析發生的次數,TCP預連接發生的次數,以及到每個主機的請求次數。這些數據就可以讓Chrome Predictor執行相應的優化決策。
除了內部事件通知外,頁面設計者可以在頁面中嵌入如下的語句請求瀏覽器進行預解析:
< link rel = "dns-prefetch" href = "http://host_name_to_prefetch.com" > |
之所以有這個需求,一個典型的例子是重定向(redirects). Chrome本身沒辦法判斷出這種模式,通過這種方式則可以讓瀏覽器提前進行解析。
具體的實現也是因版本而有所差異,總體而言,Chrome中的DNS處理有兩個主要的實現:1.基于歷史數據(historically), 通過調用平臺無關的getaddrinfo()系統函數實現。2.代理操作系統的DNS處理方法,這種方法正在被Chrome自行實現的一套異步DNS解析機制(asynchronous DNS resolver)所取代。
依賴于系統的實現,代碼少而 且簡單,但是getaddrInfo()是一個阻塞式的系統調用,無法有效地并行多個查詢操作。經驗數據還顯示,并行請求過多甚至會超出路由器的負額。 Chrome為此設計了一個復雜的機制。對于其中帶有worker-pool的預解析, Chrome只是簡單的發送getaddrinfo() 調用, 同時阻塞worker thread直到收到響應數據。因為系統有DNS緩存的原因,針對解析過的主機,解析操作會立即返回。 這個過程簡單,有效。
但還不夠!getaddrinfo()隱藏了太多有用的信息,比如Time-to-live(TTL)時間戳, DNS緩存的狀態等。于是Chrome決定自己實現一套跨平臺的異步DNS解析器。
這個新技術可以支持以下優化:
-
更好地控制重轉的時機,有能力并行執行多個查詢操作。 清晰地記錄TTLs。
-
更好地處理IPv4和IPv6的兼容。
-
基于RTT和其它事件,針對不同服務器進行錯誤處理(failover)
Chrome仍然進行著持續的優化. 通過chrome://histograms/DNS可以觀察到DNS度量數據:
上面的柱狀圖展示了 DNS預解析延遲時間的分布:比如將近50%(最右側)的查詢在20ms內完成。這些數據基于最近的瀏覽操作(采樣9869次),用戶可以選擇是否報告這 些使用數據,然后這些數據會以匿名的形式交由工程團隊加以分析,這樣就可以了解到功能的性能,以及未來如何進一步調整。周而復始,不斷優化。
使用預連接優化了TCP連接管理
已經預解析到了主機名,也有了 由OmniBox和Chrome Predictor提供信號,預示著用戶未來的操作。為什么再進一步連接到目標主機,在用戶真正發起請求前完成TCP握手呢?這樣就可省掉了另一個往返的 延遲,輕易地就能為用戶節省到上百毫秒。其實,這就是TCP預連接的工作。 通過訪問chrome://dns 就可以看到TCP預連接的使用情況。
首先, Chrome檢查它的socket pool里有沒有目標主機可以復用的socket, 這些sockets會在socket pool里保留一段時間,以節省TCP握手時間及啟動延時(slow-start penalty)。如果沒有可用的socket, 就需要發起TCP握手,然后放到socket pool中。這樣當用戶發起請求時,就可以用這個socket立即發起HTTP請求。
打開 chrome://net-internals#sockets 就可以看到當前的sockets的狀態:
你可以看到每一個socket的時間軸:連接和代理的時間,每個封包到達的時間,以及其它一些信息。你也可以把這些數據導出,以方便進一步分析或者報告問題。 有好的測試數據是優化的基礎, chrome://net-internals就是Chrome網絡的信息中心 。
使用預加載優化資源加載
Chrome支持在頁面的HTML標簽中加入的兩個線索來優化資源加載:
< link rel = "subresource" href = "/javascript/myapp.js" > < link rel = "prefetch" href = "/images/big.jpeg" > |
在rel中指定的 subresource(子資源)和prefetch(預加載)非常相似。不同的是,如果一個link指定rel(relation)為prefetch 后,就是告訴瀏覽器這個資源是稍后的頁面中要用到的。而指定為subresource則表示在本頁中就會用到,期望能在使用前加載。兩者不同的語義讓 resource loader有不同的行為。prefetch的優先級較低,一般只會在頁面加載完成后才會開始。而subresource則會在解析出來時就被嘗試優先執行。
還要注意,prefetch是HTML5的一部分,Firefox和Chrome都可以支持。但subresource還只能用在 Chrome 中。
應用Browser Prefreshing優化資源加載
不過,并不是所有的Web開發者會愿意加入上面所述的subresource relation, 就算加了,也要等收到主文檔并解析出這些內容才行,這段時間開銷依賴于服務器的響應時間和客戶端與服務器間的延遲時間,甚至要耗去上千毫秒。
和前面的預解析,預連接一樣,這里還有一個prefreshing::
-
用戶初始化一個目標頁面的請求。
-
Chrome查詢Predictor之前針對目標頁面的子資源加載,初始化一組DNS預解析,TCP預連接及資源prefreshing。
-
如是在緩存中發現之前記錄的子資源,由從磁盤中加載到內存中。
-
如果沒有或者已經過期了,就是發送網絡請求。
直到在2013年初, prefreshing還是處于早期的討論階段。如果通過數據結果分析,這個功能最終上線了,我們就可以稍晚些時候使用到它了。
使用預渲染優化頁面瀏覽
前面討論的每個優化都用來幫助減少用戶發起請求到看到頁面內容的延遲時間。多快才能帶來即時呈現的體驗呢?基于用戶體驗數據,這個時間是100毫秒,根本沒給網絡延遲留什么空間。而在100毫秒內,又怎樣渲染頁面呢?
大家可能都有這樣的體驗: 同時開多個頁簽時會明顯快于在一個頁簽中等待。瀏覽器為此提供了一個實現方式:
< link rel = "prerender" > |
這就是Chrome的預渲染( prerendering in Chrome )! 相對于只下載一個資源的“prefetch”, “prerender”會讓Chrome在一個不可見的頁簽中渲染一個頁面,包含了它所有的子資源。當用戶要瀏覽它時,這個頁簽被切到前臺,做到了即時的體驗。
可以瀏覽 prerender-test.appspot.com 來體驗一下效果,再通過chrome://net-internals/#prerender查看下歷史記錄和預連接頁面的狀態。
因為預渲染會同時消耗CPU和網絡資源,因些一定要在確信預渲染頁面會被使用到情況下才用。Google Search就在它的搜索結果里加入prerender, 因為第一個搜索結果很可能就是下一個頁面(也叫作 Google Instant Pages )
你可以使用預渲染特性,但以下限制項一定要牢記:
-
所有的進程中最多只能有一個預渲染頁。
-
HTTPS和帶有HTTP認證的頁面不可以預渲染。
-
如果請求資源需要發起非冪等(non-idempotent,idempotent request的意義為發起多次,不會帶來服務的負面響應的請求)的請求(只有GET請求)時,預渲染也不可用。
-
所有的資源的優先級都很低。
-
頁面渲染進程的使用最低的CPU優先級。
-
如果需要超過100MB的內存,將無法使用預渲染。
-
不支持HTML5多媒體元素。
預渲染只能應用于確信安全的頁面。另外JavaScript也最好在運行時使用 Page Visibility API 來判斷一下當前頁是否可見(參考 you should be doing anyway ) !
最后,總之,Chrome正逐步優化網絡延遲和用戶體驗,讓它隨著用戶的使用越來越快!
Ilya Grigorik is a web performance engineer and developer advocate on the Make The Web Fast team at Google, where he spends his days and nights on making the web fast and driving adoption of performance best practices.
Follow @igrigorik