在咱們面試過程當中,面試官常常會問到這麼一個問題,那就是從在瀏覽器地址欄中輸入URL到頁面顯示,瀏覽器到底發生了什麼?這個問題看起來是老生常談,可是這個問題回答的好壞,確實能夠很好的反映出面試者知識的廣度和深度。html
本文從瀏覽器角度來告訴你,URL後輸入後按回車,瀏覽器內部究竟發生了什麼,讀完本文後,你將瞭解到:html5
在講瀏覽器架構以前,先理解兩個概念,進程
和線程
。web
進程(process)是程序的一次執行過程,是一個動態概念,是程序在執行過程當中分配和管理資源的基本單位,線程(thread)是CPU調度和分派的基本單位,它可與同屬一個進程的其餘的線程共享進程所擁有的所有資源。面試
簡單的說呢,進程能夠理解成正在執行的應用程序,而線程呢,能夠理解成咱們應用程序中的代碼的執行器。而他們的關係可想而知,線程是跑在進程裏面的,一個進程裏面可能有一個或者多個線程,而一個線程,只能隸屬於一個進程。segmentfault
你們都知道,瀏覽器屬於一個應用程序,而應用程序的一次執行,能夠理解爲計算機啓動了一個進程
,進程啓動後,CPU會給該進程分配相應的內存空間,當咱們的進程獲得了內存以後,就可使用線程
進行資源調度,進而完成咱們應用程序的功能。瀏覽器
而在應用程序中,爲了知足功能的須要,啓動的進程會建立另外的新的進程來處理其餘任務,這些建立出來的新的進程擁有全新的獨立的內存空間,不能與原來的進程內向內存,若是這些進程之間須要通訊,能夠經過IPC機制(Inter Process Communication)來進行。緩存
不少應用程序都會採起這種多進程的方式來工做,由於進程和進程之間是互相獨立的它們互不影響
,也就是說,當其中一個進程掛掉了以後,不會影響到其餘進程的執行,只須要重啓掛掉的進程就能夠恢復運行。安全
假如咱們去開發一個瀏覽器,它的架構能夠是一個單進程多線程的應用程序,也能夠是一個使用IPC通訊的多進程應用程序。服務器
不一樣的瀏覽器使用不一樣的架構,下面主要以Chrome爲例,介紹瀏覽器的多進程架構。網絡
在Chrome中,主要的進程有4個:
這4個進程之間的關係是什麼呢?
首先,當咱們是要瀏覽一個網頁,咱們會在瀏覽器的地址欄裏輸入URL,這個時候Browser Process
會向這個URL發送請求,獲取這個URL的HTML內容,而後將HTML交給Renderer Process
,Renderer Process
解析HTML內容,解析遇到須要請求網絡的資源又返回來交給Browser Process
進行加載,同時通知Browser Process
,須要Plugin Process
加載插件資源,執行插件代碼。解析完成後,Renderer Process
計算獲得圖像幀,並將這些圖像幀交給GPU Process
,GPU Process
將其轉化爲圖像顯示屏幕。
Chrome爲何要使用多進程架構呢?
第一,更高的容錯性。當今WEB應用中,HTML,JavaScript和CSS日益複雜,這些跑在渲染引擎的代碼,頻繁的出現BUG,而有些BUG會直接致使渲染引擎崩潰,多進程架構使得每個渲染引擎運行在各自的進程中,相互之間不受影響,也就是說,當其中一個頁面崩潰掛掉以後,其餘頁面還能夠正常的運行不收影響。
第二,更高的安全性和沙盒性(sanboxing)。渲染引擎會常常性的在網絡上遇到不可信、甚至是惡意的代碼,它們會利用這些漏洞在你的電腦上安裝惡意的軟件,針對這一問題,瀏覽器對不一樣進程限制了不一樣的權限,併爲其提供沙盒運行環境,使其更安全更可靠
第三,更高的響應速度。在單進程的架構中,各個任務相互競爭搶奪CPU資源,使得瀏覽器響應速度變慢,而多進程架構正好規避了這一缺點。
以前的咱們說到,Renderer Process
的做用是負責一個Tab內的顯示相關的工做,這就意味着,一個Tab,就會有一個Renderer Process,這些進程之間的內存沒法進行共享,而不一樣進程的內存經常須要包含相同的內容。
爲了節省內存,Chrome提供了四種進程模式(Process Models),不一樣的進程模式會對 tab 進程作不一樣的處理。
這裏須要給出 site 和 site-instance 的定義
site-instance 指的是一組 connected pages from the same site,這裏 connected 的定義是 can obtain references to each other in script code 怎麼理解這段話呢。知足下面兩中狀況而且打開的新頁面和舊頁面屬於上面定義的同一個 site,就屬於同一個 site-instance
<a target="_blank">
這種方式點擊打開的新頁面window.open
)理解了概念以後,下面解釋四個進程模式
首先是Single process
,顧名思義,單進程模式,全部tab都會使用同一個進程。接下來是Process-per-tab
,也是顧名思義,每打開一個tab,會新建一個進程。而對於Process-per-site
,當你打開 a.baidu.com 頁面,在打開 b.baidu.com 的頁面,這兩個頁面的tab使用的是共一個進程,由於這兩個頁面的site相同,而如此一來,若是其中一個tab崩潰了,而另外一個tab也會崩潰。
Process-per-site-instance
是最重要的,由於這個是 Chrome 默認使用的模式,也就是幾乎全部的用戶都在用的模式。當你打開一個 tab 訪問 a.baidu.com ,而後再打開一個 tab 訪問 b.baidu.com,這兩個 tab 會使用兩個進程。而若是你在 a.baidu.com 中,經過JS代碼打開了 b.baidu.com 頁面,這兩個 tab 會使用同一個進程。
那麼爲何瀏覽器使用Process-per-site-instance
做爲默認的進程模式呢?
Process-per-site-instance
兼容了性能與易用性,是一個比較中庸通用的模式。
前面咱們講了瀏覽器的多進程架構,講了多進程架構的各類好處,和Chrome是怎麼優化多進程架構的,下面從用戶瀏覽網頁這一簡單的場景,來深刻了解進程和線程是如何呈現咱們的網站頁面的。
以前咱們咱們提到,tab之外的大部分工做由瀏覽器進程Browser Process
負責,針對工做的不一樣,Browser Process 劃分出不一樣的工做線程:
當咱們在瀏覽器的地址欄輸入內容按下回車時,UI thread
會判斷輸入的內容是搜索關鍵詞(search query)仍是URL,若是是搜索關鍵詞,跳轉至默認搜索引擎對應都搜索URL,若是輸入的內容是URL,則開始請求URL。
回車按下後,UI thread
將關鍵詞搜索對應的URL或輸入的URL交給網絡線程Network thread
,此時UI線程使Tab前的圖標展現爲加載中狀態,而後網絡進程進行一系列諸如DNS尋址,創建TLS鏈接等操做進行資源請求,若是收到服務器的301重定向響應,它就會告知UI線程進行重定向而後它會再次發起一個新的網絡請求。
network thread
接收到服務器的響應後,開始解析HTTP響應報文,而後根據響應頭中的Content-Type
字段來肯定響應主體的媒體類型(MIME Type),若是媒體類型是一個HTML文件,則將響應數據交給渲染進程(renderer process)來進行下一步的工做,若是是 zip 文件或者其它文件,會把相關數據傳輸給下載管理器。
與此同時,瀏覽器會進行 Safe Browsing 安全檢查,若是域名或者請求內容匹配到已知的惡意站點,network thread 會展現一個警告頁。除此以外,網絡線程還會作 CORB(Cross Origin Read Blocking)檢查來肯定那些敏感的跨站數據不會被髮送至渲染進程。
各類檢查完畢之後,network thread 確信瀏覽器能夠導航到請求網頁,network thread 會通知 UI thread 數據已經準備好,UI thread 會查找到一個 renderer process 進行網頁的渲染。
瀏覽器爲了對查找渲染進程這一步驟進行優化,考慮到網絡請求獲取響應須要時間,因此在第二步開始,瀏覽器已經預先查找和啓動了一個渲染進程,若是中間步驟一切順利,當 network thread 接收到數據時,渲染進程已經準備好了,可是若是遇到重定向,這個準備好的渲染進程也許就不可用了,這個時候會從新啓動一個渲染進程。
到了這一步,數據和渲染進程都準備好了,Browser Process
會向 Renderer Process
發送IPC消息來確認導航,此時,瀏覽器進程將準備好的數據發送給渲染進程,渲染進程接收到數據以後,又發送IPC消息給瀏覽器進程,告訴瀏覽器進程導航已經提交了,頁面開始加載。
這個時候導航欄會更新,安全指示符更新(地址前面的小鎖),訪問歷史列表(history tab)更新,便可以經過前進後退來切換該頁面。
當導航提交完成後,渲染進程開始加載資源及渲染頁面(詳細內容下文介紹),當頁面渲染完成後(頁面及內部的iframe都觸發了onload事件),會向瀏覽器進程發送IPC消息,告知瀏覽器進程,這個時候UI thread會中止展現tab中的加載中圖標。
導航過程完成以後,瀏覽器進程把數據交給了渲染進程,渲染進程負責tab內的全部事情,核心目的就是將HTML/CSS/JS代碼,轉化爲用戶可進行交互的web頁面。那麼渲染進程是如何工做的呢?
渲染進程中,包含線程分別是:
不一樣的線程,有着不一樣的工做職責。
當渲染進程接受到導航的確認信息後,開始接受來自瀏覽器進程的數據,這個時候,主線程會解析數據轉化爲DOM(Document Object Model)對象。
DOM爲WEB開發人員經過JavaScript與網頁進行交互的數據結構及API。
在構建DOM的過程當中,會解析到圖片、CSS、JavaScript腳本等資源,這些資源是須要從網絡或者緩存中獲取的,主線程在構建DOM過程當中若是遇到了這些資源,逐一發起請求去獲取,而爲了提高效率,瀏覽器也會運行預加載掃描(preload scanner)程序,若是HTML中存在img
、link
等標籤,預加載掃描程序會把這些請求傳遞給Browser Process
的network thread進行資源下載。
構建DOM過程當中,若是遇到<script>
標籤,渲染引擎會中止對HTML的解析,而去加載執行JS代碼,緣由在於JS代碼可能會改變DOM的結構(好比執行document.write()
等API)
不過開發者其實也有多種方式來告知瀏覽器應對如何應對某個資源,好比說若是在<script>
標籤上添加了 async
或 defer
等屬性,瀏覽器會異步的加載和執行JS代碼,而不會阻塞渲染。
DOM樹只是咱們頁面的結構,咱們要知道頁面長什麼樣子,咱們還須要知道DOM的每個節點的樣式。主線程在解析頁面時,遇到<style>
標籤或者<link>
標籤的CSS資源,會加載CSS代碼,根據CSS代碼肯定每一個DOM節點的計算樣式(computed style)。
計算樣式是主線程根據CSS樣式選擇器(CSS selectors)計算出的每一個DOM元素應該具有的具體樣式,即便你的頁面沒有設置任何自定義的樣式,瀏覽器也會提供其默認的樣式。
DOM樹和計算樣式完成後,咱們還須要知道每個節點在頁面上的位置,佈局(Layout)其實就是找到全部元素的幾何關係的過程。
主線程會遍歷DOM 及相關元素的計算樣式,構建出包含每一個元素的頁面座標信息及盒子模型大小的佈局樹(Render Tree),遍歷過程當中,會跳過隱藏的元素(display: none),另外,僞元素雖然在DOM上不可見,可是在佈局樹上是可見的。
佈局 layout 以後,咱們知道了不一樣元素的結構,樣式,幾何關係,咱們要繪製出一個頁面,咱們要須要知道每一個元素的繪製前後順序,在繪製階段,主線程會遍歷佈局樹(layout tree),生成一系列的繪畫記錄(paint records)。繪畫記錄能夠看作是記錄各元素繪製前後順序的筆記。
文檔結構、元素的樣式、元素的幾何關係、繪畫順序,這些信息咱們都有了,這個時候若是要繪製一個頁面,咱們須要作的是把這些信息轉化爲顯示器中的像素,這個轉化的過程,叫作光柵化
(rasterizing)。
那咱們要繪製一個頁面,最簡單的作法是隻光柵化視口內(viewport)的網頁內容,若是用戶進行了頁面滾動,就移動光柵幀(rastered frame)而且光柵化更多的內容以補上頁面缺失的部分,以下:
Chrome第一個版本就是採用這種簡單的繪製方式,這一方式惟一的缺點就是每當頁面滾動,光柵線程都須要對新移進視圖的內容進行光柵化,這是必定的性能損耗,爲了優化這種狀況,Chrome採起一種更加複雜的叫作合成(compositing)的作法。
那麼,什麼是合成?合成是一種將頁面分紅若干層,而後分別對它們進行光柵化,最後在一個單獨的線程 - 合成線程(compositor thread)裏面合併成一個頁面的技術。當用戶滾動頁面時,因爲頁面各個層都已經被光柵化了,瀏覽器須要作的只是合成一個新的幀來展現滾動後的效果罷了。頁面的動畫效果實現也是相似,將頁面上的層進行移動並構建出一個新的幀便可。
爲了實現合成技術,咱們須要對元素進行分層,肯定哪些元素須要放置在哪一層,主線程須要遍歷渲染樹來建立一棵層次樹(Layer Tree),對於添加了 will-change
CSS 屬性的元素,會被看作單獨的一層,沒有 will-change
CSS屬性的元素,瀏覽器會根據狀況決定是否要把該元素放在單獨的層。
你可能會想要給頁面上全部的元素一個單獨的層,然而當頁面的層超過必定的數量後,層的合成操做要比在每一個幀中光柵化頁面的一小部分還要慢,所以衡量你應用的渲染性能是十分重要的一件事情。
一旦Layer Tree被建立,渲染順序被肯定,主線程會把這些信息通知給合成器線程,合成器線程開始對層次數的每一層進行光柵化。有的層的能夠達到整個頁面的大小,因此合成線程須要將它們切分爲一塊又一塊的小圖塊(tiles),以後將這些小圖塊分別進行發送給一系列光柵線程(raster threads)進行光柵化,結束後光柵線程會將每一個圖塊的光柵結果存在GPU Process
的內存中。
爲了優化顯示體驗,合成線程能夠給不一樣的光柵線程賦予不一樣的優先級,將那些在視口中的或者視口附近的層先被光柵化。
當圖層上面的圖塊都被柵格化後,合成線程會收集圖塊上面叫作繪畫四邊形(draw quads)的信息來構建一個合成幀(compositor frame)。
以上全部步驟完成後,合成線程就會經過IPC向瀏覽器進程(browser process)提交(commit)一個渲染幀。這個時候可能有另一個合成幀被瀏覽器進程的UI線程(UI thread)提交以改變瀏覽器的UI。這些合成幀都會被髮送給GPU從而展現在屏幕上。若是合成線程收到頁面滾動的事件,合成線程會構建另一個合成幀發送給GPU來更新頁面。
合成的好處在於這個過程沒有涉及到主線程,因此合成線程不須要等待樣式的計算以及JavaScript完成執行。這就是爲何合成器相關的動畫最流暢,若是某個動畫涉及到佈局或者繪製的調整,就會涉及到主線程的從新計算,天然會慢不少。
當頁面渲染完畢之後,TAB內已經顯示出了可交互的WEB頁面,用戶能夠進行移動鼠標、點擊頁面等操做了,而當這些事件發生時候,瀏覽器是如何處理這些事件的呢?
以點擊事件(click event)爲例,讓鼠標點擊頁面時候,首先接受到事件信息的是Browser Process
,可是Browser Process只知道事件發生的類型和發生的位置,具體怎麼對這個點擊事件進行處理,仍是由Tab內的Renderer Process
進行的。Browser Process接受到事件後,隨後便把事件的信息傳遞給了渲染進程,渲染進程會找到根據事件發生的座標,找到目標對象(target),而且運行這個目標對象的點擊事件綁定的監聽函數(listener)。
前面咱們說到,合成器線程能夠獨立於主線程以外經過已光柵化的層建立組合幀,例如頁面滾動,若是沒有對頁面滾動綁定相關的事件,組合器線程能夠獨立於主線程建立組合幀,若是頁面綁定了頁面滾動事件,合成器線程會等待主線程進行事件處理後纔會建立組合幀。那麼,合成器線程是如何判斷出這個事件是否須要路由給主線程處理的呢?
因爲執行 JS 是主線程的工做,當頁面合成時,合成器線程會標記頁面中綁定有事件處理器的區域爲非快速滾動區域
(non-fast scrollable region),若是事件發生在這些存在標註的區域,合成器線程會把事件信息發送給主線程,等待主線程進行事件處理,若是事件不是發生在這些區域,合成器線程則會直接合成新的幀而不用等到主線程的響應。
而對於非快速滾動區域的標記,開發者須要注意全局事件的綁定,好比咱們使用事件委託,將目標元素的事件交給根元素body進行處理,代碼以下:
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault() } })
在開發者角度看,這一段代碼沒什麼問題,可是從瀏覽器角度看,這一段代碼給body元素綁定了事件監聽器,也就意味着整個頁面都被編輯爲一個非快速滾動區域,這會使得即便你的頁面的某些區域沒有綁定任何事件,每次用戶觸發事件時,合成器線程也須要和主線程通訊並等待反饋,流暢的合成器獨立處理合成幀的模式就失效了。
其實這種狀況也很好處理,只須要在事件監聽時傳遞passtive
參數爲 true,passtive
會告訴瀏覽器你既要綁定事件,又要讓組合器線程直接跳過主線程的事件處理直接合成建立組合幀。
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault() } }, {passive: true});
當合成器線程接收到事件信息,斷定到事件發生不在非快速滾動區域後,合成器線程會向主線程發送這個時間信息,主線程獲取到事件信息的第一件事就是經過命中測試(hit test)去找到事件的目標對象。具體的命中測試流程是遍歷在繪製階段生成的繪畫記錄(paint records)來找到包含了事件發生座標上的元素對象。
通常咱們屏幕的幀率是每秒60幀,也就是60fps,可是某些事件觸發的頻率超過了這個數值,好比wheel,mousewheel,mousemove,pointermove,touchmove,這些連續性的事件通常每秒會觸發60~120次,假如每一次觸發事件都將事件發送到主線程處理,因爲屏幕的刷新速率相對來講較低,這樣使得主線程會觸發過量的命中測試以及JS代碼,使得性能有了不必是損耗。
出於優化的目的,瀏覽器會合並這些連續的事件,延遲到下一幀渲染是執行,也就是requestAnimationFrame
以前。
而對於非連續性的事件,如keydown,keyup,mousedown,mouseup,touchstart,touchend等,會直接派發給主線程去執行。
瀏覽器的多進程架構,根據不一樣的功能劃分了不一樣的進程,進程內不一樣的使命劃分了不一樣的線程,當用戶開始瀏覽網頁時候,瀏覽器進程進行處理輸入、開始導航請求數據、請求響應數據,查找新建渲染進程,提交導航,以後渲染又進行了解析HTML構建DOM、構建過程加載子資源、下載並執行JS代碼、樣式計算、佈局、繪製、合成,一步一步的構建出一個可交互的WEB頁面,以後瀏覽器進程又接受頁面的交互事件信息,並將其交給渲染進程,渲染進程內主進程進行命中測試,查找目標元素並執行綁定的事件,完成頁面的交互。
本文大部份內容也是對inside look at modern web browser系列文章的整理、解讀和翻譯吧,整理過程仍是收穫很是大的,但願讀者讀了本文只有有所啓發吧。