窺探現代瀏覽器架構(二)

前言

本文是筆者對Mario Kosaka寫的inside look at modern web browser系列文章的翻譯。這裏的翻譯不是指直譯,而是結合我的的理解將做者想表達的意思表達出來,並且會盡可能補充一些相關的內容來幫助你們更好地理解。git

導航的時候都發生了什麼

這篇文章是探究Chrome內部工做原理的四集系列文章中的第二篇,在上一篇文章中咱們探討了不一樣進程或者線程是如何負責瀏覽器各個不一樣部分的工做的。在這篇文章中,咱們將會深刻了解每一個進程和線程是如何溝通協做來爲咱們呈現出網站內容的。github

讓咱們來看一個用戶瀏覽網頁最簡單的情景:你在瀏覽器導航欄裏面輸入一個URL而後按下回車鍵,瀏覽器接着會從互聯網上獲取相關的數據並把網頁展現出來。在本篇文章中,咱們將會重點關注這個簡單場景中網站數據請求以及瀏覽器在呈現網頁以前作的準備工做 - 也就是導航(navigation)的過程。web

一切都從瀏覽器進程開始

咱們在上一篇文章CPU,GPU,內存和多進程架構中提到,瀏覽器中tab外面發生的一切都是由瀏覽器進程(browser process)控制的。瀏覽器進程有不少負責不一樣工做的線程(worker thread),其中包括繪製瀏覽器頂部按鈕和導航欄輸入框等組件的UI線程(UI thread)、管理網絡請求的網絡線程(network thread)、以及控制文件讀寫的存儲線程(storage thread)等。當你在導航欄裏面輸入一個URL的時候,其實就是UI線程在處理你的輸入。 api

UI,網絡和存儲線程都是屬於瀏覽器進程的瀏覽器

一次簡單的導航

第一步:處理輸入

當用戶開始在導航欄上面輸入內容的時候,UI線程(UI thread)作的第一件事就是詢問:「你輸入的字符串是一些搜索的關鍵詞(search query)仍是一個URL地址呢?」。由於對於Chrome瀏覽器來講,導航欄的輸入既多是一個能夠直接請求的域名還多是用戶想在搜索引擎(例如Google)裏面搜索的關鍵詞信息,因此當用戶在導航欄輸入信息的時候UI線程要進行一系列的解析來斷定是將用戶輸入發送給搜索引擎仍是直接請求你輸入的站點資源。 緩存

UI線程在詢問輸入的字符串是搜索關鍵詞仍是一個URL安全

第二步:開始導航

當用戶按下回車鍵的時候,UI線程會叫網絡線程(network thread)初始化一個網絡請求來獲取站點的內容。這時候tab上會展現一個提示資源正在加載中的旋轉圈圈,並且網絡線程會進行一系列諸如DNS尋址以及爲請求創建TLS鏈接的操做。 服務器

UI線程告訴網絡線程跳轉到mysite.com網絡

這時若是網絡線程收到服務器的HTTP 301重定向響應,它就會告知UI線程進行重定向而後它會再次發起一個新的網絡請求。session

第三步:讀取響應

網絡線程在收到HTTP響應的主體(payload)流(stream)時,在必要的狀況下它會先檢查一下流的前幾個字節以肯定響應主體的具體媒體類型(MIME Type)。響應主體的媒體類型通常能夠經過HTTP頭部的Content-Type來肯定,不過Content-Type有時候會缺失或者是錯誤的,這種狀況下瀏覽器就要進行MIME類型嗅探來肯定響應類型了。MIME類型嗅探並非一件容易的事情,你能夠從Chrome的源代碼的註釋來了解不一樣瀏覽器是如何根據不一樣的Content-Type來判斷出主體具體是屬於哪一個媒體類型的。

響應的頭部有Content-Type信息,而響應的主體有真實的數據

若是響應的主體是一個HTML文件,瀏覽器會將獲取的響應數據交給渲染進程(renderer process)來進行下一步的工做。若是拿到的響應數據是一個壓縮文件(zip file)或者其餘類型的文件,響應數據就會交給下載管理器(download manager)來處理。

網絡線程在詢問響應的數據是否是來自安全源的HTML文件

網絡線程在把內容交給渲染進程以前還會對內容作SafeBrowsing檢查。若是請求的域名或者響應的內容和某個已知的病毒網站相匹配,網絡線程會給用戶展現一個警告的頁面。除此以外,網絡線程還會作CORBCross Origin Read Blocking)檢查來肯定那些敏感的跨站數據不會被髮送至渲染進程。

第四步:尋找一個渲染進程(renderer process)

在網絡線程作完全部的檢查後而且可以肯定瀏覽器應該導航到該請求的站點,它就會告訴UI線程全部的數據都已經被準備好了。UI線程在收到網絡線程的確認後會爲這個網站尋找一個渲染進程(renderer process)來渲染界面。

網絡線程告訴UI線程去尋找一個渲染進程來渲染界面

因爲網絡請求可能須要長達幾百毫秒的時間才能完成,爲了縮短導航須要的時間,瀏覽器會在以前的一些步驟裏面作一些優化。例如在第二步中當UI線程發送URL連接給網絡線程後,它其實已經知曉它們要被導航到哪一個站點了,因此在網絡線程幹活的時候,UI線程會主動地爲這個網絡請求啓動一個渲染線程。若是一切順利的話(沒有重定向之類的東西出現),網絡線程準備好數據後頁面的渲染進程已經就準備好了,這就節省了新建渲染進程的時間。不過若是發生諸如網站被重定向到不一樣站點的狀況,剛剛那個渲染進程就不能被使用了,它會被摒棄,一個新的渲染進程會被啓動。

第五步:提交(commit)導航

到這一步的時候,數據和渲染進程都已經準備好了,瀏覽器進程(browser process)會經過IPC告訴渲染進程去提交本次導航(commit navigation)。除此以外瀏覽器進程還會將剛剛接收到的響應數據流傳遞給對應的渲染進程讓它繼續接收到來的HTML數據。一旦瀏覽器進程收到渲染線程的回覆說導航已經被提交了(commit),導航這個過程就結束了,文檔的加載階段(document loading phase)會正式開始。

到了這個時候,導航欄會被更新,安全指示符(security indicator)和站點設置UI(site settings UI)會展現新頁面相關的站點信息。當前tab的會話歷史(session history)也會被更新,這樣當你點擊瀏覽器的前進和後退按鈕也能夠導航到剛剛導航完的頁面。爲了方便你在關閉了tab或窗口(window)的時候還能夠恢復當前tab和會話(session)內容,當前的會話歷史會被保存在磁盤上面。

瀏覽器進程經過IPC來對渲染進程發起渲染頁面的請求

額外步驟:初始加載完成(Initial load complete)

當導航提交完成後,渲染進程開始着手加載資源以及渲染頁面。我會在後面系列文章中講述渲染進程渲染頁面的具體細節。一旦渲染進程「完成」(finished)渲染,它會經過IPC告知瀏覽器進程(注意這發生在頁面上全部幀(frames)的onload事件都已經被觸發了並且對應的處理函數已經執行完成了的時候),而後UI線程就會中止導航欄上旋轉的圈圈。

我這裏用到「完成」這個詞,由於後面客戶端的JavaScript仍是能夠繼續加載資源和改變視圖內容的。

渲染進程經過IPC告訴瀏覽器進程頁面已經加載完成了

導航到不一樣的站點

一個最簡單的導航情景已經描述完了!但是若是這時用戶在導航欄上輸入一個不同的URL會發生什麼呢?若是是這樣,瀏覽器進程會從新執行一遍以前的那幾個步驟來完成新站點的導航。不過在瀏覽器進程作這些事情以前,它須要讓當前的渲染頁面作一些收尾工做,具體就是詢問一下當前的渲染進程需不須要處理一下beforeunload事件。

beforeunload能夠在用戶從新導航或者關閉當前tab時給用戶展現一個「你肯定要離開當前頁面嗎?」的二次確認彈框。瀏覽器進程之因此要在從新導航的時候和當前渲染進程確認的緣由是,當前頁面發生的一切(包括頁面的JavaScript執行)是不受它控制而是受渲染進程控制,因此它也不知道里面的具體狀況。

注意:不要隨便給頁面添加beforeunload事件監聽,你定義的監聽函數會在頁面被從新導航的時候執行,所以這會增長重導航的時延。beforeunload事件監聽函數只有在十分必要的時候才能被添加,例如用戶在頁面上輸入了數據,而且這些數據會隨着頁面消失而消失。

瀏覽器進程經過IPC告訴渲染進程它將要離開當前頁面導航到新的頁面了

若是從新導航是在頁面內被髮起的呢?例如用戶點擊了頁面的一個連接或者客戶端的JavaScript代碼執行了諸如window.location = "newsite.com"的代碼。這種狀況下,渲染進程會本身先檢查一個它有沒有註冊beforeunload事件的監聽函數,若是有的話就執行,執行完後發生的事情就和以前的狀況沒什麼區別了,惟一的不一樣就是此次的導航請求是由渲染進程給瀏覽器進程發起的。

若是是從新導航到不一樣站點(different site)的話,會有另一個渲染進程被啓動來完成此次重導航,而當前的渲染進程會繼續處理如今頁面的一些收尾工做,例如unload事件的監聽函數執行。Overview of page lifecycle states這篇文章會介紹頁面全部的生命週期狀態,the Page Lifecycle API會教你如何在頁面中監聽頁面狀態的變化。

瀏覽器進程告訴新的渲染進程去渲染新的頁面而且告訴當前的渲染進程進行收尾工做

Service Worker的情景

這個導航過程最近發生的一個改變是引進了service worker的概念。由於Service worker能夠用來寫網站的網絡代理(network proxy),因此開發者能夠對網絡請求有更多的控制權,例如決定哪些數據緩存在本地以及哪些數據須要從網絡上面從新獲取等等。若是開發者在service worker裏設置了當前的頁面內容從緩存裏面獲取,當前頁面的渲染就不須要從新發送網絡請求了,這就大大加快了整個導航的過程。

這裏要重點留意的是service worker其實只是一些跑在渲染進程裏面的JavaScript代碼。那麼問題來了,當導航開始的時候,瀏覽器進程是如何判斷要導航的站點存不存在對應的service worker並啓動一個渲染進程去執行它的呢?

其實service worker在註冊的時候,它的做用範圍(scope)會被記錄下來(你能夠經過文章The Service Worker Lifecycle瞭解更多關於service worker做用範圍的信息)。在導航開始的時候,網絡線程會根據請求的域名在已經註冊的service worker做用範圍裏面尋找有沒有對應的service worker。若是有命中該URL的service worker,UI線程就會爲這個service worker啓動一個渲染進程(renderer process)來執行它的代碼。Service worker既可能使用以前緩存的數據也可能發起新的網絡請求。

網絡線程會在收到導航任務後尋找有沒有對應的service worker

UI線程會啓動一個渲染進程來運行找到的service worker代碼,代碼具體是由渲染進程裏面的工做線程(worker thread)執行

導航預加載 - Navigation Preload

在上面的例子中,你應該能夠感覺到若是啓動的service worker最後仍是決定發送網絡請求的話,瀏覽器進程和渲染進程這一來一回的通訊包括service worker啓動的時間其實增長了頁面導航的時延。導航預加載就是一種經過在service worker啓動的時候並行加載對應資源的方式來加快整個導航過程效率的技術。預加載資源的請求頭會有一些特殊的標誌來讓服務器決定是發送全新的內容給客戶端仍是隻發送更新了的數據給客戶端。

UI線程在啓動一個渲染進程去運行service worker代碼的同時會並行發送網絡請求

總結

在本篇文章中,咱們討論了導航具體都發生了哪些事情以及瀏覽器優化導航效率採起的一些技術方案,在下一篇文章中咱們將會深刻了解瀏覽器是如何解析咱們的HTML/CSS/JavaScript來呈現出網頁內容的。

持續關注個人技術動態

我是進擊的大蔥,關注我和我一塊兒進步成獨當一面的全棧工程師!

文章首發於:窺探現代瀏覽器架構(二)

關注個人我的公衆號獲取個人最新技術推送!

相關文章
相關標籤/搜索