[譯] JavaScript 是如何工作的:深入網路層 + 如何優化效能和安全

Hopsken發表於2019-03-04

這是探索 JavaScript 及其內建元件系列文章的第 12 篇。在認識和描述這些核心元素的過程中,我們也會分享我們在構建 SessionStack 時所遵循的一些經驗規則。SessionStack 是一個輕量級 JavaScript 應用,它協助使用者實時檢視和復現他們的 Web 應用缺陷,因此其自身不僅需要足夠健壯還要有不俗的效能表現。

如果你錯過了前面的文章,你可以在下面找到它們:

  1. [譯] JavaScript 是如何工作的:對引擎、執行時、呼叫堆疊的概述
  2. [譯] JavaScript 是如何工作的:在 V8 引擎裡 5 個優化程式碼的技巧
  3. [譯] JavaScript 是如何工作的:記憶體管理 + 處理常見的4種記憶體洩漏
  4. [譯] JavaScript 是如何工作的: 事件迴圈和非同步程式設計的崛起 + 5個如何更好的使用 async/await 編碼的技巧
  5. [譯] JavaScript 是如何工作的:深入剖析 WebSockets 和擁有 SSE 技術 的 HTTP/2,以及如何在二者中做出正確的選擇
  6. [譯] JavaScript 是如何工作的:與 WebAssembly 一較高下 + 為何 WebAssembly 在某些情況下比 JavaScript 更為適用
  7. [譯] JavaScript 是如何工作的:Web Worker 的內部構造以及 5 種你應當使用它的場景
  8. [譯] JavaScript 是如何工作的:Web Worker 生命週期及用例
  9. [譯] JavaScript 是如何工作的:Web 推送通知的機制
  10. [譯] JavaScript 是如何工作的:用 MutationObserver 追蹤 DOM 的變化
  11. [譯] JavaScript 是如何工作的:渲染引擎和效能優化技巧

正如我們在前一篇關於渲染引擎的文章中所說的,我們相信,優秀的 JavaScript 開發者和傑出的 JavaScript 開發者之間的區別在於後者不僅懂得如何使用這門語言,還能夠理解它的內在以及周遭環境。

一點點歷史

49 年前,一個叫做 ARPAnet 的東西被創造了出來。它是一個早期的資料包交換網路,也是第一個實踐 TCP/IP 套件的網路。該網路在加州大學和史丹佛研究中心之間搭建了一個連線。20年後,Tim Berners-Lee 釋出了一個名為『Mesh』的提案,也就是後來人們所說的全球資訊網。在這 49 年裡,網際網路走過了漫長的道路,從兩臺計算機交換資料包開始,到如今擁有超過 7500 萬臺伺服器,38 億名使用者和 13 億個網站。

[譯] JavaScript 是如何工作的:深入網路層 + 如何優化效能和安全

在這邊文章中,我們將嘗試分析現代瀏覽器使用了哪些技術來自動地提高效能(有些你甚至並不知道)。我們將尤其關注瀏覽器的網路層。在最後,我們將會提供一些建議,關於如何使得瀏覽器能夠更好地提升你的 Web 應用的效能。

概覽

為了能夠快速、高效並且安全地展示 Web 應用/網站,現代的瀏覽器都是經過特別設計的。數百個元件執行在不同的層上,從程式管理和安全沙盒到 GPU 流水線,音訊和視訊等等,Web瀏覽器看起來更像是一個作業系統,而不僅僅是一個軟體應用程式。

瀏覽器的整體效能取決於許多大型元件:解析、佈局、樣式計算、JavaScript 和 WebAssembly 執行、渲染,當然還有網路棧

工程師經常認為網路棧是一個瓶頸。通常來說,確實如此,因為在執行接下來的步驟之前,先得從網際網路上獲取到所有的資源。為了提高網路層的效率,它不僅需要扮演簡單的套接字管理員的角色。它呈現給我們的只是一種非常簡單的資源獲取機制,但它實際上是一個擁有自己的優化標準,API 和服務的完整平臺。

[譯] JavaScript 是如何工作的:深入網路層 + 如何優化效能和安全

作為 Web 開發人員,我們不必操心個別的 TCP 或 UDP 資料包、請求格式化、快取和此過程中的其他所有事情。所以複雜的事務都由瀏覽器處理,因此我們可以專注於我們正在開發的應用程式。但是,瞭解底層究竟發生了什麼,可以幫助我們建立更快、更安全的應用程式。

實質上,當使用者開始與瀏覽器互動時發生了以下事務:

  • 使用者在瀏覽器位址列中輸入一個 URL
  • 給定一個 Web 資源的 URL,瀏覽器首先檢查本地和應用程式快取,並嘗試使用本地副本來完成請求。
  • 如果快取無法使用,瀏覽器將從URL中獲取域名,並通過 DNS 請求伺服器的 IP 地址。如果該域被快取,則不需要 DNS 查詢。
  • 瀏覽器建立一個 HTTP 資料包,說明它請求位於遠端伺服器上的某個網頁。
  • 資料包被髮送到 TCP 層,在 HTTP 資料包的頂部新增它自己的資訊。此資訊將被用於維護已經開始的會話。
  • 然後將資料包交給 IP 層,它的主要工作是找出將資料包從使用者傳送到遠端伺服器的途徑。這些資訊也會儲存在資料包的頂部。
  • 資料包被髮送到遠端伺服器。
  • 遠端伺服器一旦接收到資料包,就會以類似的方式發回響應。

W3C 的導航時序規範提供了瀏覽器 API,它能夠提供瀏覽器中每個請求的生命週期背後的時間和效能資料。讓我們來看看這些元件,因為它們在提供最佳使用者體驗方面起著至關重要的作用:

[譯] JavaScript 是如何工作的:深入網路層 + 如何優化效能和安全

這個網路通訊的過程是非常複雜的,有很多不同的層可能成為瓶頸。這就是為什麼瀏覽器努力通過使用各種技術來提高效能的原因,以便整個網路通訊的影響最小。

套接字管理

讓我們先從一些術語開始:

  • 源(Origin) —— 由應用協議、域名、埠三者構成(例如,https,www.example.com,443)
  • 套接字池(Socket pool) —— 一組屬於同一源的套接字(所有主流瀏覽器都將池的大小限制為最多 6 個套接字)

JavaScript 和 WebAssembly 不允許我們管理網路套接字的生命週期,這是一件好事!這不僅可以使我們免去很多麻煩,而且還可以讓瀏覽器自動去進行大量的效能優化,其中一些包括套接字重用,請求優先順序和後期繫結,協議協商,強制連線限制等等。

實際上,現代瀏覽器更進了一步,把請求管理週期與套接字管理分立了開來。套接字按池組織,按源分組,每個池強制實施自己的連線限制和安全約束。待處理的請求會先排隊,再按優先順序處理,然後繫結到池中的單個套接字上。除非伺服器有意關閉連線,否則可以在多個請求中自動重用相同的套接字!

[譯] JavaScript 是如何工作的:深入網路層 + 如何優化效能和安全

由於開闢新的 TCP 連線需要額外的成本,因此連線的重用具有很大的效能優勢。預設情況下,瀏覽器使用所謂的「keepalive」機制,這可以節省出在已有請求發生後再開啟新連線到伺服器的時間。開啟一個新的 TCP 連線的平均時間是:

  • 本地請求 —— 23ms
  • 橫貫大陸的請求 —— 120ms
  • 洲際請求 —— 225ms

這種架構為其他一些優化提供了可能。這些請求可以根據其優先順序以不同的順序執行。瀏覽器可以優化所有套接字的頻寬分配,或者在預期請求時先開啟套接字。

正如我之前提到的,這一切都是由瀏覽器自行管理的,並不需要我們做任何工作。但這並不一定意味著我們什麼都做不了。選擇合適的網路通訊模式,傳輸型別和頻率,恰當地選擇協議以及調整/優化伺服器架構可以在提高應用程式的整體效能方面發揮重要作用。

有些瀏覽器甚至更進一步。例如,Chrome 可以學習使用者的操作習慣來使自己變得更快。它根據使用者訪問的網站和典型的瀏覽模式進行學習,以便在使用者做任何事情之前預測可能的使用者行為並採取行動。最簡單的例子是當使用者在連結上懸停時,Chrome 會預先渲染頁面。如果您有興趣瞭解有關 Chrome 優化的更多資訊,可以檢視高效能瀏覽器網路(High-Performance Browser Networking)一書中的本章節 www.igvita.com/posa/high-p…

網路安全和沙盒

允許瀏覽器管理單個套接字具有另一個非常重要的目的:通過這種方式,瀏覽器可以對不可信的應用程式資源強制執行一致的安全和策略約束。例如,瀏覽器不允許通過 API 直接訪問原始網路套接字,因為這可以使任何惡意應用程式與任何主機進行任意連線。瀏覽器還強制性地限制連線數,以保護伺服器以及客戶端免受資源耗盡的問題。

瀏覽器格式化所有傳出請求,以強制實行風格一致且格式良好的協議語義來保護伺服器。同樣,響應解碼自動完成,以保護使用者免受惡意伺服器的侵害。

TLS 協商

傳輸層安全協定(TLS)是一種在計算機網路上提供安全通訊保障的加密協議。它在許多應用程式中廣泛使用,其中之一是網頁瀏覽。網站可以使用 TLS 來保護其伺服器和 Web 瀏覽器之間的所有通訊。

完整的 TLS 握手包含以下幾步:

  1. 客戶端向伺服器傳送『Client hello』訊息,與之一同傳送的還有客戶端產生的隨機值和支援的密碼套件。
  2. 伺服器通過向客戶端傳送『Server hello』訊息以及伺服器產生的隨機值進行響應。
  3. 伺服器將其認證證照傳送給客戶端,並可能向客戶端請求類似的證照。伺服器傳送『Server hello done』訊息。
  4. 如果伺服器已經向客戶端請求了證照,則客戶端傳送它。
  5. 客戶端建立一個隨機的預主金鑰(Pre-Master Secret),並使用伺服器證照中的公鑰對其進行加密,再將加密的預主金鑰傳送給伺服器。
  6. 伺服器接收到預主金鑰。伺服器和客戶端根據預主金鑰生成主金鑰和會話金鑰。
  7. 客戶端向伺服器傳送『Change cipher spec』通知,指示客戶端將開始使用新的會話金鑰進行雜湊和加密訊息。客戶端還傳送『Client finished』訊息。
  8. 伺服器收到『Change cipher spec』的訊息,並使用會話金鑰將其記錄層安全狀態切換為對稱加密。伺服器向客戶端傳送『Server finished』訊息。
  9. 客戶端和伺服器現在可以通過他們建立的安全通道交換應用程式資料。所有從當前客戶端傳送到伺服器並返回的訊息均使用會話金鑰加密。

任何一步校驗失敗,使用者都將會收到警告。例如,伺服器正在使用自簽名證照。

同源策略

如果兩個頁面的協議、埠和主機名都相同的話,那麼這兩個頁面同源。

以下是一些可能嵌入跨源資源的一些例子:

  • 通過 <script src=”…”></script> 引用 JavaScript 資源。語法錯誤的錯誤訊息僅適用於同源指令碼
  • 通過 <link rel=”stylesheet” href=”…”> 引用 CSS 資源。由於 CSS 的寬鬆語法規則,跨源 CSS 需要正確的 Content-Type 標頭。不同瀏覽器可能有不同的限制。
  • 通過 <img> 引用影象資源。
  • 通過 <video><audio> 引用多媒體資源。
  • 通過 <object><embed><applet> 引用外掛資源。
  • 通過 @font-face 引用字型資源。某些瀏覽器允許使用跨域字型,某些則不行。
  • 任何通過 <frame><iframe> 引用的資源。網站可以使用 X-Frame-Options 頭部標識來阻止這種形式的跨源互動。

以上列表遠非完整;其目地是為了突出『最小特權』原則。 瀏覽器只公開應用程式程式碼必須的 API 和資源:應用程式提供資料和 URL,瀏覽器格式化請求並處理每個連線的完整生命週期。

值得注意的是,『同源策略』並非是個單一的概念。相反,有一組相關機制來強制性地限制 DOM 訪問,Cookie 和會話狀態管理,網路以及瀏覽器的其他元件。

資源和客戶端狀態快取

最好和最快的請求是未發出的請求。在分派請求之前,瀏覽器會自動檢查其資源快取,執行必要的驗證檢查,並在滿足指定條件時返回資源的本地副本。如果本地資源在快取中不可用,則會發出網路請求,並且響應會被自動放入快取中以供後續訪問(如果允許)。

  • 瀏覽器自動評估每個資源上的快取指令
  • 在可能的情況下,瀏覽器會自動重新驗證過期資源
  • 瀏覽器自動管理快取大小和回收資源

管理高效和優化的資源快取是很困難的。值得慶幸的是,瀏覽器替我們完成了所有複雜的事務,我們只需要確保我們的伺服器返回適當的快取指令;要了解更多資訊,請參見客戶端上的快取資源(Cache Resources on the Client)。您確實有為網頁上的所有資源都提供了 Cache-Control,ETag 和 Last-Modified 響應頭部欄位,對吧?

最後,瀏覽器經常被忽視但至關重要的功能是提供身份驗證,會話和 cookie 管理。瀏覽器為每個源維護單獨的「Cookie jars」,提供必要的應用程式和伺服器 API 來讀取和寫入新的 Cookie,會話和身份驗證資料,並自動附加上和處理相應的 HTTP 頭以代替我們自動執行整個過程。

舉個栗子:

用一個簡單但有說明性的例子來說明將會話狀態管理推放到瀏覽器端的便利之處:同一個經過身份驗證的會話可以在多個選項卡或瀏覽器視窗之間共享,反之亦然;單個選項卡中的登出操作將使所有其他開啟的視窗中開啟的會話失效。

應用程式 API 和協議

研究完了網路服務,終於到達了應用程式 API 和協議這一步。正如我們所看到的,較低層提供了一系列關鍵服務:套接字和連線管理、請求和響應處理、各種安全策略的執行、快取等等。每次我們啟動一個 HTTP、一個XMLHttpRequest 或是一個長期的 Server-Sent Event 或 WebSocket 會話,或是開啟一個 WebRTC 連線,我們都在與這些底層服務的一部分或全部進行互動。

沒有單一的最佳協議或 API。每個不平凡的應用程式都需要根據各種需求混合使用不同的傳輸:與瀏覽器快取的互動、協議開銷、訊息延遲、可靠性、資料傳輸型別等等。某些協議可能提供低延遲傳輸(例如 Server-Sent Events,WebSocket),但可能不符合其他關鍵條件,例如在所有情況下都能夠利用瀏覽器快取或支援高效二進位制傳輸。

簡單幾步提高您的 Web 應用效能和安全性

  • 請求中始終使用「Connection:Keep-Alive」頭部欄位。瀏覽器預設這樣做。確保伺服器使用相同的機制。
  • 使用正確的 Cache-Control、Etag 和 Last-Modified 頭部欄位,這樣可以節約一些瀏覽器下載時間。
  • 花時間調整並優化您的 Web 伺服器。這才是真正的魔法發生的地方!請記住,該過程要針對每個 Web 應用程式以及您要傳輸的資料的型別對症下藥。
  • 始終使用 TLS!特別是如果您的應用程式中有任何形式的身份驗證。
  • 研究瀏覽器在您的應用程式中提供並實施了哪些安全策略。

效能和安全性兩者都是 SessionStack 中的一等公民。我們無法妥協的原因在於,一旦 SessionStack 整合到您的 Web 應用程式中,它就會開始監視從 DOM 更改、使用者互動到網路請求,未處理的異常和除錯訊息的所有內容。所有資料都會實時傳輸到我們的伺服器上,這樣您就能夠以視訊的形式重現使用者遇到的一切情況。 而這一切都是以最短的延遲進行的,不會對應用程式造成任何額外的效能開銷。

這就是為何我們努力實踐以上所有提示,以及我們將在未來發布的內容中討論的更多內容。

如果你想試一試 SessionStack,這有一個免費的計劃。

[譯] JavaScript 是如何工作的:深入網路層 + 如何優化效能和安全

參考資源


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章