1. 程式人生 > 實用技巧 >[譯]Google Chrome中的高效能網路

[譯]Google Chrome中的高效能網路

>>> hot3.png

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):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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執行相應的優化決策。

除了內部事件通知外,頁面設計者可以在頁面中嵌入如下的語句請求瀏覽器進行預解析:

1 2 < link rel = "dns-prefetch" href = "//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標籤中加入的兩個線索來優化資源載入:

1 2 3 < 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毫秒內,又怎樣渲染頁面呢?

大家可能都有這樣的體驗: 同時開多個頁籤時會明顯快於在一個頁籤中等待。瀏覽器為此提供了一個實現方式:

1 2 < link rel = "prerender" href = "http://example.org/index.html" >

這就是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 Grigorikis 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

轉載於:https://my.oschina.net/boltwu/blog/421939