原文: Inside Look at Modern Web Browser (part 2)做者: Mariko Kosakaweb
譯者: kyrieliusegmentfault
本文是這個系列的第二篇文章,會深刻到 Chrome 的內部工做。在上一篇文章中,咱們瞭解了線程和進程在瀏覽器中的不一樣,而在這篇文章中,咱們會更加深刻的瞭解當瀏覽器爲用戶呈現一個頁面時,這些進程和線程之間是如何通訊的。 api
讓咱們以一個常見的例子做爲起點:輸入一個 url,瀏覽器會從服務端獲取數據並將頁面展現出來。本文會聚焦在用戶經過瀏覽器向一個站點發起訪問請求以及瀏覽器準備渲染這個頁面的部分,這個過程我稱之爲導航。瀏覽器
咱們在上一篇文章中提過,全部處於窗口以外的部分都由同一個瀏覽器進程進行掌管。瀏覽器的進程又同時擁有許多線程,掌管瀏覽器的不一樣部分:UI 線程用來繪製頂部的操做按鈕和輸入框、網絡線程負責處理並接收來自互聯網的數據、存儲線程控制着訪問本地文件的權限等。當你將一個網站的 url 輸入到瀏覽器的地址欄時,此刻正是瀏覽器進程中的 UI 線程在起做用。緩存
當用戶開始在地址欄輸入時,UI 線程首先會問:「大兄弟,你輸入的是個查詢字符串仍是網站地址?」。由於 Chrome 的地址欄同時仍是個搜索框,因此 UI 線程須要解析用戶的輸入,才能決定該直接訪問網址仍是把用戶的輸入丟給搜索引擎處理。安全
當用戶按下回車鍵後,UI 線程要求網絡線程去獲取網站的內容。窗口的 Tab 上會開始轉菊花,網絡線程會採用一系列的協議和操做(好比 DNS)查詢必要的信息併爲請求創建鏈接。 服務器
此時,網絡線程可能會收到來自服務器的一個標記着重定向指令的頭部好比 HTTP 301,在這種狀況下,網絡線程會把這件事情告訴 UI 線程,以後則會發起一次指向重定向地址的新的網絡請求。網絡
當響應的數據開始傳送到瀏覽器時,網絡線程會在必要的狀況下檢查一些來自響應的字段。響應數據的 Content-Type
字段會表示當前返回的是哪一種類型的數據,但它也不徹底靠譜,常常會出現丟失或者乾脆不許確的狀況,但也不用擔憂,MIME 嗅探會完成確缺失的工做。正如源碼的註釋中寫道,這是一個能夠被解釋爲 hack 的方案,若是感興趣的話,你也能夠去閱讀這些註釋,這樣就能瞭解不一樣的瀏覽器是如何將實際的數據與 Content-Type
匹配了。 session
若是響應數據是一個 HTML 文件,那麼接下來的一步會是把數據傳遞給瀏覽器的渲染進程;但若是數據是 zip 壓縮文件或其餘類型的文件,意味着這將被定位成一次下載動做,因而瀏覽器會將數據轉交給下載管理器去處理。 多線程
一般這一步也是安全檢測發生的時候:若是域名或響應數據和已知的惡意網站匹配時,網絡進程會拋出一個警告,並展示一個告警的頁面。另外,CORB 檢測也會開始工做,確保那些來自敏感站點的跨站響應數據不會進入到瀏覽器的渲染進程中。
網絡線程以獲取了所有的數據,並完成了全部須要的檢查,此刻它自信的告訴 UI 線程:「小兄弟,數據準備好了!」。接着,UI 線程會喚起一個渲染進程去渲染頁面。
因爲網絡狀況的不可控,一個請求可能會花上好幾百毫秒才能把響應數據拿回來,因此這裏瀏覽器默認開啓了用來加速這一過程的優化。在 Step 2 中,當 UI 線程將須要請求的 url 告訴網絡線程時,其實它自己已經知道要導航到哪一個網站了,因而 UI 線程在把 url 傳遞給網絡線程的同時,會嘗試啓動一個渲染進程。若是一切都按照預期正常進行的話,當網絡線程拿到數據時,渲染進程就已經處於待命狀態了。也會有例外的狀況:好比導航重定向到一個另外的站點,那麼預先啓動好的渲染進程將不會被使用,這致使 UI 線程須要從新啓動一個渲染進程。
如今咱們假設數據和渲染進程都準備好了,瀏覽器進程經過 IPC 告知渲染進程能夠出發本次導航了。與此同時,數據流也將傳遞給渲染進程,這樣後者就能繼續接收 HTML 數據。一旦瀏覽器收到了來自渲染進程的導航啓動信號,此次導航也就完成了,下一步進入文檔的加載階段。
到這會兒,瀏覽器的地址欄更新,安全指示符和站點的設置 UI 會將新頁面的信息呈現出來。當前窗口的 session 將會更新,剛導航到的頁面會被後退/前進按鈕記錄到窗口的頁面歷史中。爲了便於在關閉窗口時恢復頁面,歷史的會話記錄會保存在本地的磁盤上。
當導航觸發後,渲染進程會持續接收資源並渲染頁面。咱們將在下一篇文章中討論這一步的更多細節。當渲染進程「完成」渲染後,它會經過 IPC 告知瀏覽器進程(頁面的 onload 事件均已執行完畢後),UI 線程也就再也不在 tab 上轉菊花了。
上面的「完成」兩個字,之因此打了雙引號,由於在實際場景中,它一般並不真正意味着完成,由於客戶端的 JavaScript 可能在此時持續地加載資源並渲染新的視圖。
一次簡單的導航截至目前已經完成了。假如這時用戶輸入了一個不一樣的 url 會發生什麼呢?其實也沒啥,瀏覽器進程會按照上面的步驟導航到這個網站。但在這一切開始以前,瀏覽器會檢查當前已經渲染好了的網站是否須要在網頁卸載以前搞一點事情,這就是 beforeunload
事件。
在 beforeunload
事件中,咱們能夠在用戶即將跳轉至其餘頁面或者關閉 Tab 的時候發起一個「確認離開當前頁面?」的二次確認。Tab 中的全部東西都由渲染進程控制着,固然也包括開發者編寫的 JavaScript,因此當一個新的導航請求即將到來時,瀏覽器進程會對當前的渲染進程作最後的檢查。
咱們應當儘可能避免在 beforeunload
中添加總會執行的事件代碼,這會形成更多的交互延時,畢竟它們總會在新的導航開始以前執行。只在須要的時候添加這些代碼,好比提醒用戶若是進入新的頁面那麼當前頁面的數據會丟失。
若是導航是在渲染進程中被建立的(好比用戶點擊了頁面上的某一連接或者在 JavaScript 運行了 window.location.href = 'https://kyrieliu.cn'
),則當前的渲染進程會首先檢查是 beforeunload
中是否有東西須要執行。以後,它會經歷與瀏覽器進程直接發起導航後同樣的導航過程。
當新的導航將發往與當前頁面不一樣的站點時,瀏覽器將會建立一個新的渲染進程去處理這些新工做,舊的渲染進程則則用來在剩餘的時間裏處理諸如 unload
的頁面事件。若是你想了解更多的話,能夠看看頁面生命週期概覽和頁面生命週期 API這兩篇文章。
Service Worker 的引入會對頁面的導航流程帶來一些改變。Service Worker 是一種能夠在應用代碼中編寫網絡代理的方法;加強了開發者對於本地緩存以及什麼時候發起網絡請求的控制。若是 Service Worker 提早設置了從本地緩存中讀取某一頁面的數據,那麼也就不須要發起網絡請求了。
須要明確的一點是,即便 Service Worker 提供了聽起來很高端的功能,但它實質上也是運行在渲染進程中的 JavaScript 代碼。那麼問題來了:當用戶發起一次導航時,瀏覽器進程是如何知道目標站點存在一個 Service Worker 的呢?
當一個 Service Worker 註冊後,它的做用域會保存在一個引用中(你能夠經過 Service Worker 的生命週期 這篇文章瞭解我所說的「做用域」)。當導航發生時,網絡線程會依據域名在已註冊的 Service Worker 做用域集合中查詢,若是找到某個對應的 Service Worker,UI 線程會發起一個渲染進程去執行 Service Worker 中的代碼。Service Worker 能夠從本地緩存中加載數據(無需發起網絡請求),也能夠選擇經過網絡請求獲取最新的資源和數據。
相信你能夠發現,若是 Service Worker 最終決定從網絡中請求數據,那麼以前在瀏覽器進程和渲染進程之間所發生的通訊都將成爲致使響應延時的罪魁禍首。導航預加載就是用來加速這一進程的機制:與 Service Worker 並行啓動去加載資源。它將爲這些請求設置一個 Header,又服務端來決定爲這些請求發送不一樣的內容;好比,僅返回更新的數據而不是整個文檔。
在這篇文章中,咱們檢視了在導航時都發生了什麼,以及 Web 應用的代碼好比響應頭和客戶端的 JavaScript 代碼是如何與瀏覽器進行交互的。 瞭解了瀏覽器是如何一步步從網絡中請求數據的,這能讓咱們更好的理解不少 API 好比導航預加載的誕生初衷。
在下一篇文章中,咱們會深刻討論瀏覽器是如何執行 HTML/CSS/JavaScript 代碼從而完成一個頁面的渲染的。