49 年前,ARPnet 建立了。這是一個早期的分組交換網路,也是第一個 實現了 TCP/IP 協議簇 的網路。該網路建立了一個從加州大學到史丹佛研究院的連線。20 年後,Tim Berners-Lee(譯註:全球資訊網之父)分享了一個叫做 “Mesh” 的提案(譯註:參看 Information Management: A Proposal),這在之後成為了我們所熟知的全球資訊網(World Wide Web)。49 年間,因特網得到了長足發展,從僅僅是兩臺電腦間的資料分組交換,到現如今有超過 7500 萬臺伺服器,38 億個網際網路使用者,以及 13 億個網站。
本文中,我們將分析現代瀏覽器用來自動提升效能的技術(甚至你都感知不到這些技術),並且我們會聚焦於瀏覽器的網路層。我們也會提供一些使用瀏覽器提高你的 web 應用效能的思路。最後,我們也會分享一些我們在構建 SessionStack 時的經驗法則,這是一個輕量級、健壯且高效能的 JavaScript 應用,旨在幫助使用者實時檢視和復現他們的 web 應用缺陷。
我們都熟悉這 13 億個網站在呈現一個使用者友好頁面時所用的技術。這次我們則聚焦於 web 瀏覽器。現代 web 瀏覽器被專門設計來交付快速、高效和安全的 web 應用程式或是網站。web 瀏覽器看起來更像是一個作業系統,而不僅僅是一個軟體,因為有數以百計的元件執行在不同分層,從程式管理和安全沙箱,再到 GPU 管道,音視訊等等。
瀏覽器的整體效能取決於這些大型元件:解析、佈局、樣式計算、JavaScript 和 WebAssembly 執行、渲染以及網路堆疊。網路堆疊(或者說網路協議棧)經常會被質疑是效能的瓶頸所在。這是因為在剩餘步驟被解鎖前,需要從因特網獲得所有需要的資源。為了讓網路層高效,網路堆疊需要扮演一個更為重要的角色,而不僅僅是個簡單的 socket 管理員。網路層的資源獲取的機制是簡單淺顯的,但機制以外,它還是一個擁有自己的優化法則、API 以及服務的完整平臺。
作為 web 開發者,我們不用關心各個 TCP 或者 UDP 報文,請求格式化、快取等等正在進行的過程。這些複雜的東西都是瀏覽器的職責,這讓我們可以專注於應用開發。但是,這也不妨礙我們多多少少去知道一些 web 瀏覽器的底層細節。事實上,這可以幫助我們建立更快、更安全的應用。
本質上,下面羅列的這些就是使用者和瀏覽器互動的過程:
- 使用者在瀏覽器位址列輸入了一段 URL。
- 瀏覽器從 URL 中獲得了域名,再通過 DNS 請求到伺服器的 IP 地址。
- 瀏覽器建立了一個 HTTP 報文,該報文說明了它將請求放在遠端伺服器上的 web 頁面。
- 報文被送到了 TCP 層,TCP 層會在 HTTP 報文頭部新增一些它自己的資訊,該資訊是保持會話的必需。
- 之後,報文又被送入了 IP 層,這一層的主要任務是指出如何將你的報文從本地傳送到遠端的伺服器。這個資訊也被儲存在了報文頭部。
- 報文被送到了遠端伺服器。
- 一旦報文被收到,服務端響應會被以同樣形式送回。
這是一個針對於網路請求建立後發生了什麼而做的高階概述。整個網路程式是非常複雜的,其中許多層都可能成為效能瓶頸。這也就是為何瀏覽器會致力於通過使用不同的技術手段來減小網路通訊的開銷,從而提高效能。
socket 管理
讓我們以幾個技術開始:
- origin —— 一個含有應用協議、域名和埠的三元組(例如 https、www.example.com、443)
- socket 池 —— 一個同源 sockets 組(所有的主流瀏覽器都限制了池的大小不超過 6 個 socket)
JavaScript 和 WebAssembly 不允許我們管理單個報文的生命期,這可是件好事兒!這不僅讓我們專注於應用開發,還允許瀏覽器自動進行一系列的效能提升,例如 socket 重用、設定請求優先順序以及延遲繫結、協議協商、強制連線限制等等。事實上,現代瀏覽器已經極大地將請求管理迴圈從 socket 管理中分離出來。Socket 通過池進行組織,每個池容納了同源的 socket,每一個 socket 池又都強制了連線限制和安全限制。待執行請求被放入佇列並設定了優先順序,之後會被繫結到池中單個 socket。除非伺服器有意關閉了連線,否則相同的 socket 可以在多個請求中自動重用。
由於開啟一個新的 TCP 連線會帶來額外的效能開銷,因此連線重用會為連線帶來極大的效能收益。預設情況下,當一個請求建立後,為避免開啟一個新的到伺服器的連線產生的耗時,瀏覽器使用了 “keepalive” 機制。開啟一個本地請求的 TCP 連線的平均時間為 23 ms,開啟一個橫貫大陸連線的平均時間為 120 ms,而開啟一個洲際連線則為 225 ms。現在,想象瀏覽器已經建立了 10 個到伺服器的連線,你大可自己算算要消耗多少時間。
這一架構為其他許多效能優化手段開啟了大門。不同優先順序的請求,將會被以不同的順序執行。瀏覽器可以優化各個 socket 間的頻寬分配,也可以依據請求開啟新的 socket。
正如我之前提到的,這些都是通過瀏覽器進行管理,而不會要求開發者做任何的工作。但這並不意味著我們對於提升網路效能無能為力。選擇正確的網路通訊模式、型別、傳輸頻率,以及伺服器堆疊的選擇和除錯都將在應用的整體效能中扮演重要角色。
一些瀏覽器的能力甚至不僅於此。例如,Chrome 的自我學習手段能讓你越用越快。它是基於使用者已訪問過的網站和具有代表性的瀏覽模式進行學習的,因此,它可以預估相似使用者的行為,並且在使用者什麼都沒做之前就進行優化。最簡單的例子就是,當使用者的滑鼠滑過某個超連結時,Chrome 就預先渲染了這個連結對應的頁面。如果你想要了解更多 Chrome 的優化手段,你可以閱讀 High-Performace Browser Networking 的這一章 www.igvita.com/posa/high-p…。
網路安全和沙箱化
允許瀏覽器對單獨的 socket 進行管理還有另外一個重要目的:它為不受信任的應用資源強制開啟了一連串的安全和策略限制。例如,瀏覽器不允許 API 直接訪問原始的網路 socket,因為這將讓任何惡意應用都能直連到任意主機。瀏覽器也強行限制了連線個數,目的在於防止伺服器和客戶端資源枯竭。
瀏覽器會對所有發出的請求進行格式化,藉此強制協議語義的一致性和結構正確,從而保護伺服器。類似地,響應解碼也會自動完成,從而保護使用者不受惡意伺服器的侵害。
TLS 協議
傳輸層安全(TLS) 是一個加密協議,它能夠在計算機網路間提供通訊安全。TLS 已經被廣泛應用到了許多應用中,其中之一就是 web 瀏覽。網站可以使用 TLS 來保障伺服器和 web 瀏覽器間的通訊安全。
完整的 TLS 握手過程包含如下步驟:
- 客戶端傳送了一個 “Client hello” 訊息給伺服器,並附上了客戶端的隨機數和支援的密文簇。
- 伺服器響應一個 “Server hello” 訊息給客戶端,並附上了服務端的隨機數。
- 伺服器傳送其證照給客戶端用於認證,並且也請求客戶端的證照。然後,伺服器傳送 “Server hello done” 訊息。
- 如果伺服器向客戶端請求了證照,則客戶端就會傳送證照。
- 客戶端建立了一個隨機的 Pre-Master Secret,並且使用了從伺服器證照中獲得的公鑰對其進行加密,之後傳送加密後的 Pre-Master Secret 給伺服器。
- 伺服器收到了 Pre-Master Secret。基於 Pre-Master Secret,伺服器和客戶端各自產生了 Master Secret 和 session keys。
- 客戶端傳送了 “Change cipher spec” 通知到伺服器,以此指明客戶端將會開始使用新的 session keys 來加密訊息和雜湊化訊息。客戶端也會傳送一個 “Client finished” 訊息給伺服器。
- 伺服器收到了 “Change cipher spec” 訊息,然後將其記錄層安全狀態轉換為使用 session key 的對稱加密。然後伺服器傳送了 “Server finished” 訊息給客戶端。
- 客戶端和伺服器現在可以在它們所建立的安全通道上進行應用資料的交換,所有客戶端和伺服器間的訊息都使用了 session key 進行加密。
流程中如果有任何的校驗失敗 —— 例如伺服器使用了自簽名的證照,使用者都將會被警告。
同源策略
瀏覽器強制對應用程式能夠初始化的請求在型別上做出了限制,也強制對請求的源做了限制。
上面羅列的也遠不夠完整。同源策略的目的在於強調 “最小特權” 原則生效了。瀏覽器只暴露了應用程式碼所必需的 API 和資源:應用所用的資料、URL,瀏覽器格式化了請求並且操縱了每個連線完整的生命週期。
值得注意的是,“同源策略” 尚沒有一個簡單的概念,取而代之的是,有一系列相關的機制來強制對 DOM 訪問、cookie 和 session 狀態管理、網路、以及另外一些的瀏覽器元件做出限制。如果你對此仍存有疑惑,我建議你看看 Michal Zalewski 的 The Tangled Web。
資源及客戶端狀態快取
最好、最快的請求就是不做請求。在分發一個請求前,瀏覽器會自動檢查資源快取並進行必要的校驗,如果滿足特定的條件,則直接返回本地快取的資源備份。類似地,如果沒有命中本地快取中的資源,就會傳送一個網路請求,得到的響應將自動地放入快取中服務於後續的訪問。
- 瀏覽器會自動評估每個資源的快取指令
- 瀏覽器會在可能的時候自動對過期資源進行再驗證
- 瀏覽器會自動管理快取大小並進行資源回收
手動管理一個高效的,最優化的資源快取是非常困難的。幸運地是,瀏覽器自己承擔了這份複雜的工作,我們只需要保證我們的伺服器返回正確的快取指令即可。想要了解更多的話,可以參看 Cache Resources on the Client。你為頁面的所有資源都提供了一個 Cache-Control,ETag 以及 Last-Modified 響應頭,對吧?
最後,瀏覽器的一個常被忽略卻至關重要的功能就是提供了認證、session(會話) 和 cookie 管理。瀏覽器為每個源維護了相互隔離的 “cookie jars(餅乾罐)”,並暴露了應用和伺服器所需的 API 來讀寫新的 cookie、session 以及認證資料,又通過自動新增和處理了正確的 HTTP 頭部來幫助我們實現整個過程的自動化。
舉個例子:
一個簡單的例證就是將 session 狀態管理推遲到瀏覽器所帶來的便捷:一個已認證的 session 可以在多個瀏覽器標籤頁或者視窗中共享,反之亦然。在某個標籤頁進行的登出操作也會讓所有其他的標籤頁或者視窗中對應的 session 失效。
應用層 API 和 協議
順著網路服務的梯子一步步爬,最終我們將到達應用層,接觸到應用層 API 和 協議。
正如我們所看到的,較低層的網路層提供了應用廣泛而又關鍵的服務:socket 和連線管理、請求和響應處理、各種強制性的安全策略、快取等等。每當我們初始化一個 HTTP 或者 XMLHttpRequest、一個長期存活的 Server-Sent Event 或者 WebSocket 會話、或者開啟了一個 WebRTC 連線,我們都會和這些底層服務互動。
當然,不存在一個最好的協議和 API。每個大型應用都會混合不同的傳輸方式,這是基於各種各樣的需求:和瀏覽器快取互動、協議過載、訊息延遲、應用可靠性、資料傳輸型別等等。一些協議可能提供低延遲的交付能力(例如 Server-Sent Events、WebSocket),但這可能又不滿足其他的關鍵準則,例如利用瀏覽器快取的能力或者在所有場景下都支援高效的二進位制傳輸。
概括下來,有以下手段可以提高你的 web 應用效能和安全:
- 總在你的請求頭部使用 “Connection: Keep-Alive” 。瀏覽器預設會做這件事兒。你要確定你的伺服器也使用了同樣的機制。
- 使用合適的 Cache-Control、ETag 和 Last-Modified 頭部,藉此你可以節省不少瀏覽器下載時間。
- 花費一些時間來除錯和優化你的 web 伺服器。
- 總是使用 TLS!特別是如果你的應用程式使用了任意型別的認證手段。
- 研究一下你所用的瀏覽器都提供了哪些安全策略,並在你的應用中強制使用它們。
- 務必瀏覽下本文參考資料中提及的書籍。可以從其中學到其他的技術。
在 SessionStack 中,效能和安全同屬一等公民。二者被置於如此高的層面進行考慮的原因是,一旦 SessionStack 嵌入你的 web 應用,它就開始記錄你應用的每一件事兒,從 DOM 變化和使用者互動,到未捕獲的異常和 debug 資訊。所有這些資料都實時地傳入我們的伺服器,這讓你能夠通過視訊重現你應用的每個問題,看見每一件發生在使用者身上的事兒。所有的這些都具有最小的延時,也不會對你的應用造成任何的效能過載。
這就是為什麼我們致力於在 SessionStack 利用上述所有的,以及未來博文中將討論的建議。
這裡有一個免費計劃讓你開始使用我們的產品。
參考資料
- hpbn.co/
- www.amazon.com/Tangled-Web…
- msdn.microsoft.com/en-us/libra…
- www.internetlivestats.com/
- vanseodesign.com/web-design/…
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。