瀏覽器渲染機制

線程和進程

進程和線程的概念能夠這樣理解:css

進程是一個工廠,工廠有它的獨立資源--工廠之間相互獨立--線程是工廠中的工人,多個工人協做完成任務--工廠內有一個或多個工人--工人之間共享空間

工廠有多個工人,就至關於一個進程能夠有多個線程,並且線程共享進程的空間。html

clipboard.png

進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位,系統會給它分配內存)
線程是cpu調試的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程。核心仍是屬於一個進程。)前端

瀏覽器是多進程的

clipboard.png

瀏覽器是多進程的,每打開一個tab頁,就至關於建立了一個獨立的瀏覽器進程。ios

瀏覽器包含的進程:

  1. Browser進程:瀏覽器的主進程(負責協調,主控),只有一個,做用有:es6

    • 負責瀏覽器的界面顯示,與用戶交互,如前進,後退等
    • 負責各個頁面的管理,建立和銷燬其它進程
    • Rendered進程獲得的內存中的Bitmap,繪製到用戶界面上
    • 網絡資源的管理,下載
  2. 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才建立。
  3. GPU進程:最多一個,用於3D繪製等。
  4. 瀏覽器渲染進程(瀏覽器內核)(Render進程,內部是多線程的):默認每一個Tab頁面一個進程,互不影響。主要做用爲:web

    • 頁面渲染,腳本執行,事件處理等

在瀏覽器中打開一個網頁至關於新起了一個進程(進程內有本身的多線程)ajax

瀏覽器多進程的優點

  • 避免單個page crash影響整個瀏覽器
  • 避免第三方插件crash影響整個瀏覽器
  • 多進程充分利用多核優點
  • 方便使用沙盒模型隔離插件等進程,提升瀏覽器穩定性

簡單理解就是:若是瀏覽器是單進程的,某個Tab頁崩潰了,就影響了整個瀏覽器,體驗就會不好。同理若是是單進程的,插件崩潰了也會影響整個瀏覽器;
固然,內存等資源消耗也會更大,像空間換時間同樣。canvas

重點是瀏覽器內核(渲染進程)

對於普通的前端操做來講,最重要的渲染進程:頁面的渲染,js的執行,事件的循環等都在這個進程內執行;api

瀏覽器是多進程的,瀏覽器的渲染進程是多線程的;promise

GUI渲染線程

  • 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
  • 當界面須要重繪或因爲某種操做引起迴流時,該線程就會執行。
  • 注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(至關於凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。

JS引擎線程

  • 也稱爲JS內核,負責處理JavaScript腳本程序。(例如V8引擎)。
  • JS引擎線程負責解析JavaScript腳本,運行代碼。
  • JS引擎一直等待着任務隊列中任務的到來,而後加以處理,一個Tab頁(render進程)中不管何時都只有一個JS線程在運行JS程序。
  • 一樣注意,GUI渲染線程與JS引擎線程是互斥的,因此若是JS執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞。

事件觸發線程

  • 歸屬於瀏覽器而不是JS引擎,用來控制事件循環(能夠理解成JS引擎本身都忙不過來,須要瀏覽器另開線程協助)。
  • JS引擎執行代碼塊如setTimeout時(也可來自瀏覽器內核的其它線程,如鼠標點擊,AJAX異步請求等),會將對應任務添加到事件線程中。
  • 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理。
  • 注意,因爲JS的單線程關係,因此這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)。

定時觸發器線程

  • 傳說中的setTimeoutsetInterval所在的線程
  • 瀏覽器定時計數器並非由JavaScript引擎計數的,(由於JavaScript引擎是單線程的,若是處於阻塞線程狀態就會影響計時的準確)
  • 所以經過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行)
  • 注意,W3CHTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms

異步http請求線程

  • XMLHttpRequest在鏈接後是經過瀏覽器新型一個線程請求
  • 將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中,再由JavaScript引擎執行

總結下來,渲染進程以下:

clipboard.png

Browser主進程和瀏覽器內核(渲染進程)的通訊過程

打開一個瀏覽器,能夠看到:任務管理器出現了2個進程(一個主進程,一個是打開Tab頁的渲染進程);

  • Browser主進程收到用戶請求,首先須要獲取頁面內容(如經過網絡下載資源),隨後將該任務經過RendererHost接口傳遞給Render渲染進程
  • Render渲染進程的Renderer接口收到消息,簡單解釋後,交給渲染線程GUI,而後開始渲染
  • GUI渲染線程接收請求,加載網頁並渲染網頁,這其中可能須要Browser主進程獲取資源和須要GPU進程來幫助渲染
  • 固然可能會有JS線程操做DOM(這可能會形成迴流並重繪)
  • 最後Render渲染進程將結果傳遞給Browser主進程
  • Browser主進程接收到結果並將結果繪製出來

clipboard.png

瀏覽器內核(渲染進程)中線程之間的關係

GUI渲染線程與JS引擎線程互斥

因爲JavaScript是可操做DOM的,若是在修改這些元素屬性同時渲染界面(即JS線程和GUI線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。

所以,爲了防止渲染出現不可預期的結果,瀏覽器就設置了互斥的關係,當JS引擎執行時GUI線程會被掛起。GUI更新則會被保存在一個隊列中等到JS引擎線程空閒時當即被執行。

JS阻塞頁面加載

從上述的互斥關係,能夠推導出,JS若是執行時間過長就會阻塞頁面。

譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存在隊列中,要等到JS引擎空閒後執行。而後因爲巨量計算,因此JS引擎可能好久好久才能空閒,確定就會感受很卡。

因此,要儘可能避免JS執行時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞的感受。

css加載是否會阻塞dom樹渲染

這裏說的是頭部引入css的狀況
首先,咱們都知道:css是由單獨的下載線程異步下載的。
而後還有幾個現象:

  1. css加載不會阻塞DOM樹解析(異步加載時dom照常構建)
  2. 但會阻塞render樹渲染(渲染時須要等css加載完畢,由於render樹須要css信息)

這可能也是瀏覽器的一種優化機制
由於你加載css的時候,可能會修改下面DOM節點的樣式,若是css加載不阻塞render樹渲染的話,那麼當css加載完以後,render樹可能又得從新重繪或者回流了,這就形成了一些沒有必要的損耗
因此乾脆把DOM樹的結構先解析完,把能夠作的工做作完,而後等css加載完以後,在根據最終的樣式來渲染render樹,這種作法確實對性能好一點。

WebWorker,JS的多線程?

前文中有提到JS引擎是單線程的,並且JS執行時間過長會阻塞頁面,那麼JS就真的對cpu密集型計算無能爲力麼?

因此,後來HTML5中支持了WebWorker

這樣理解下:

建立Worker時,JS引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,徹底受主線程控制,並且不能操做DOM
JS引擎線程與worker線程間經過特定的方式通訊(postMessage API,須要經過序列化對象來與線程交互特定的數據)

因此,若是有很是耗時的工做,請單獨開一個Worker線程,這樣裏面無論如何翻天覆地都不會影響JS引擎主線程,只待計算出結果後,將結果通訊給主線程便可,perfect!

並且注意下,JS引擎是單線程的,這一點的本質仍然未改變,Worker能夠理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。

WebWorkerSharedWorker

既然都到了這裏,就再提一下SharedWorker(避免後續將這兩個概念搞混)

WebWorker只屬於某個頁面,不會和其餘頁面的Render進程(瀏覽器內核進程)共享
因此ChromeRender進程中(每個Tab頁就是一個render進程)建立一個新的線程來運行Worker中的JavaScript程序。

SharedWorker是瀏覽器全部頁面共享的,不能採用與Worker一樣的方式實現,由於它不隸屬於某個Render進程,能夠爲多個Render進程共享使用
因此Chrome瀏覽器爲SharedWorker單首創建一個進程來運行JavaScript程序,在瀏覽器中每一個相同的JavaScript只存在一個SharedWorker進程,無論它被建立多少次。

看到這裏,應該就很容易明白了,本質上就是進程和線程的區別。SharedWorker由獨立的進程管理,WebWorker只是屬於render進程下的一個線程

總結瀏覽器渲染流程

瀏覽器輸入 url,瀏覽器主進程接管,開一個下載線程,而後進行 http請求(略去 DNS查詢, IP尋址等等操做),而後等待響應,獲取內容,隨後將內容經過 RendererHost接口轉交給 Render進程--瀏覽器渲染流程開始

瀏覽器內核拿到內容後,渲染大概能夠劃分爲:

  1. 解析html創建dom
  2. 解析css構建render樹(將css代碼解析成樹形的數據結構,而後結合dom合併成render樹)
  3. 佈局render樹(Layout/reflow),負責各元素尺寸,位置的計算
  4. 繪製render樹(paint),繪製頁面像素信息
  5. 瀏覽器會將各層的信息發送給GPUGPU會將各層合成(composite),顯示在屏幕上

渲染完畢後就是load事件了,以後就是本身的JS邏輯處理了,略去了詳細步驟。

load事件與DOMContentLoaded事件的前後

上面提到,渲染完畢後會觸發load事件,那麼你能分清楚load事件與DOMContentLoaded事件的前後麼?

很簡單,知道它們的定義就能夠了:

DOMContentLoaded 事件觸發時,僅當DOM加載完成,不包括樣式表,圖片。
(譬如若是有async加載的腳本就不必定完成)

onload 事件觸發時,頁面上全部的DOM,樣式表,腳本,圖片都已經加載完成了。(渲染完畢了)

因此,順序是:DOMContentLoaded -> load

普通圖層和複合圖層

渲染步驟就提到了composite概念;瀏覽器渲染的圖層通常包含兩大類:普通圖層以及複合圖層。

  1. 普通文檔流內能夠理解爲一個複合圖層(這裏默認複合層,裏面無論添加多少元素,其實都是在同個複合圖層中)
  2. absolute佈局(fixed也同樣),雖然能夠脫離文檔流,但它仍然屬於默認複合層
  3. 能夠經過硬件加速的方式,聲明一個新的複合圖層,它會單獨分配資源(固然也會脫離普通文檔流,這樣一來,無論這個複合圖層中怎麼變化,也不會影響默認複合層裏的迴流重繪)

能夠簡單理解下:GPU中,各個複合圖層是單獨繪製的,因此互不影響,這也是爲何某些場景硬件加速效果一級棒

如何變成複合圖層(硬件加速)

將元素變成一個複合圖層,就是傳說中的硬件加速技術

  • 最經常使用的方式:translate3d,translatez
  • opacity屬性/過渡動畫(須要動畫執行的過程當中纔會建立合成層,動畫沒有開始或結束後元素還會回到以前的狀態)
  • will-chang屬性(這個比較偏僻),通常配合opacitytranslate使用(並且經測試,除了上述能夠引起硬件加速的屬性外,其它屬性並不會變成複合層),做用是提早告訴瀏覽器要變化,這樣瀏覽器會開始作一些優化工做(這個最好用完後就釋放)
  • <video><iframe><canvas><webgl>等元素
  • 其它,譬如之前的flash插件

absolute和硬件加速的區別

能夠看到,absolute雖然能夠脫離普通文檔流,可是沒法脫離默認複合層。

因此,就算absolute中信息改變時不會改變普通文檔流中render樹,可是,瀏覽器最終繪製時,是整個複合層繪製的,因此absolute中信息的改變,仍然會影響整個複合層的繪製。(瀏覽器會重繪它,若是複合層中內容多,absolute帶來的繪製信息變化過大,資源消耗是很是嚴重的)

而硬件加速直接就是在另外一個複合層了(另起爐竈),因此它的信息改變不會影響默認複合層(固然了,內部確定會影響屬於本身的複合層),僅僅是引起最後的合成(輸出視圖)

複合圖層的做用

通常一個元素開啓硬件加速後會變成複合圖層,能夠獨立於普通文檔流中,改動後能夠避免整個頁面重繪,提高性能。
可是儘可能不要大量使用複合圖層,不然因爲資源消耗過分,頁面反而會變的更卡。

硬件加速時請使用index

使用硬件加速時,儘量的使用index,防止瀏覽器默認給後續的元素建立複合層渲染
具體的原理是:
webkit CSS3中,若是這個元素添加了硬件加速,而且index層級比較低,那麼在這個元素的後面其它元素(層級比這個元素高的,或者相同的,而且relectiveabsolute屬性相同的),會默認變爲複合層渲染,若是處理不當會極大的影響性能

簡單點理解,能夠認爲是一個隱式合成的概念:若是a是一個複合層,並且b在a上面,那麼b也會被隱式轉爲一個複合圖層,這點須要特別注意

Event LoopJS的運行機制

到此時,已是屬於瀏覽器頁面初次渲染完畢後的事情,JS引擎的一些運行機制分析。主要是結合Event Loop來談JS代碼是如何執行的。
咱們已經知道了JS引擎是單線程的,知道了JS引擎線程,事件觸發線程,定時觸發器線程。
而後還須要知道:

  • JS分爲同步任務和異步任務
  • 同步任務都在主線程上執行,造成一個執行棧
  • 主線程以外,事件觸發線程管理着一個任務隊列,只要異步任務有了運行結果,就在任務隊列之中放置一個事件
  • 一旦執行棧中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧,開始執行。

clipboard.png

看到這裏,應該就能夠理解了:爲何有時候setTimeOut推入的事件不能準時執行?由於可能在它推入到事件列表時,主線程還不空閒,正在執行其它代碼,因此就必須等待,天然有偏差。

clipboard.png

主線程在運行時會產生執行棧,棧中的代碼調用某些api時,它們會在事件隊列中添加各類事件(當知足觸發條件後,如ajax請求完畢)。而當棧中的代碼執行完畢,就會去讀取事件隊列中的事件,去執行那些回調,如此循環。

定時器

上面事件循環機制的核心是:JS引擎線程和事件觸發線程

調用setTimeout後,是由定時器線程控制等到特定時間後添加到事件隊列的,由於JS引擎是單線程的,若是處於阻塞線程狀態就會影響計時準確,所以頗有必要另開一個線程用來計時。

當使用setTimoutsetInterval時,須要定時器線程計時,計時完成後就會將特定的事件推入事件隊列中。

如:

setTimeout(()=>console.log('hello!),1000)
//等1000毫秒計時完畢後(由定時器線程計時),將回調函數推入事件隊列中,等待主線程執行

setTimeout(()=>{
    console.log('hello')
},0)
console.log('begin')

這段代碼的效果是最快的時間內將回調函數推入事件隊列中,等待主線程執行。

注意:

  1. 執行結果是:先begin,後hello
  2. 雖然代碼的本意是0毫秒就推入事件隊列,可是W3CHTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算爲4ms
  3. 就算不等待4ms,就算假設0毫秒就推入事件隊列,也會先執行begin(由於只能可執行棧內空了後纔會主動讀取事件隊列)

setInterval

setTimeout模擬按期計時和直接用setInterval是有區別的:

  • 每次setTimeout計時到後就會去執行,而後執行一段時間後纔會繼續setTimeout,中間就多了偏差
  • setInterval則是每次都精確的隔一段時間推入一個事件(可是,事件的實際執行時間不必定就準確,還有多是這個事件還沒執行完畢,下一個事件就來了)

並且setInterval有一些比較致命的問題:

  • 累積效應,若是setInterval代碼在setInterval再次添加到隊列以前尚未完成執行,就會致使定時器代碼連續運行好幾回,而之間沒有間隔,就算正常間隔執行,多個setInterval的代碼執行時間可能會比預期小(由於代碼執行須要必定時間)
  • 好比你ioswebview,或者safari等瀏覽器中都有一人特色,在滾動的時候是不執行JS的,若是使用了setInterval,會發如今滾動結束後會執行屢次因爲滾動不執行JS積攢回調,若是回調執行時間過長,就會很是容易形成卡頓問題和一些不可知的錯誤(setInterval自帶的優化,若是當前事件隊列中有setInterval的回調,不會重複添加回調)
  • 並且把瀏覽器最小化顯示等操做時,setInterval並非不執行程序,它會把setInterval的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間所有執行

因此,至於這麼問題,通常認爲的最佳方案是:用setTimeout模擬setInterval或者特殊場合直接用requestAnimationFrame

Promise時代的microtaskmacrotask

es6盛行的如今,能夠看下這題:

console.log('script start');

setTimeout(()=>{
    console.log('setTimeout')
},0);

Promise.resolve()
.then(()=>console.log('promise1'))
.then(()=>console.log('promise2'))

console.log('script end')

//執行結果:
script start
script end
promise1
promise2
setTimeout

由於promise有一個新的概念microtask.或者能夠說JS中分爲兩種任務:macrotaskmicrotask;
理解以下:

  • macrotask(又叫宏任務),主代碼塊,setTimeout,setInterval等(能夠看到,事件隊列中的每個事件都是一個macrotask
  • 能夠理解是每次執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)
  • 第一個macrotask會從頭至尾將這個任務執行完畢,不會執行其它
  • 瀏覽器爲了可以使得JS內部macrotaskDOM任務可以有序的執行,會在一個macrotask執行結束後,在下一個macrotask執行開始前,對頁面進行從新渲染(task->渲染->task->...)
  • microtask(又叫微任務),Promise,process.nextTick等。
  • 能夠理解是在當前macrotask執行結束後當即執行的任務
  • 也就是說在當前macrotask任務後,下一個macrotask以前,在渲染以前
  • 因此它的響應速度相比setTimeout(setTimeoutmacrotask)會更快由於無需等待渲染
  • 也就是說,在某一個macrotask執行完成後,就會將在它執行期間產生的全部microtask都執行完畢(在渲染前)

注意:在Node環境下,process.nextTick的優先級高於promise.也就是:在宏任務結束後會先執行微任務隊列中的nextTick部分,而後纔會執行微任務中的promise部分。

另外,setImmediate則是規定:在下一次Event Loop(宏任務)時觸發(因此它是屬於優先級較高的宏任務),(Node.js文檔中稱,setImmediate指定的回調函數,老是排在setTimeout前面),因此setImmediate若是嵌套的話,是須要通過多個Loop才能完成的,而不會像process.nextTick同樣沒完沒了。

能夠理解:

  • macrotask中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護.
  • microtask中的全部微任務都是添加到微任務隊列中,等待當前macrotask執行完後執行,而這個隊列由JS引擎線程維護。

因此:

  • 執行一個宏任務(棧中沒有就從事件隊列中獲取)
  • 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
  • 宏任務執行完畢後,當即執行當前微任務隊列中的全部微任務(依次執行)
  • 當前宏任務執行完畢,開始檢查渲染,而後GUI線程接管渲染
  • 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)

clipboard.png

clipboard.png

new Promise裏的函數是直接執行的算作主程序裏,並且.then後面的纔會放到微任務中。

另外,請注意下Promisepolyfill與官方版本的區別:

官方版本中,是標準的microtask形式
polyfill,通常都是經過setTimeout模擬的,因此是macrotask形式
請特別注意這兩點區別

注意,有一些瀏覽器執行結果不同(由於它們可能把microtask當成macrotask來執行了),可是爲了簡單,這裏不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能並不標準)

Mutation Observer能夠用來實現microtask(它屬於microtask,優先級小於Promise,通常是Promise不支持時纔會這樣作)

它是HTML5中的新特性,做用是:監聽一個DOM變更,當DOM對象樹發生任何變更時,Mutation Observer會獲得通知

像之前的Vue源碼中就是利用它來模擬nextTick的,具體原理是,建立一個TextNode並監聽內容變化,而後要nextTick的時候去改一下這個節點的文本內容,以下:(Vue的源碼,未修改)

var counter=1
var observer=newMutationObserver(nextTickHandler)
var textNode=document.createTextNode(String(counter))
observer.observe(textNode,{characterData:true})
timerFunc=()=>{
    counter=(counter+1)%2
    textNode.data=String(counter)
}

不過,如今的Vue(2.5+)nextTick實現移除了Mutation Observer的方式(聽說是兼容性緣由),取而代之的是使用MessageChannel(固然,默認狀況仍然是Promise,不支持才兼容的)。

MessageChannel屬於宏任務,優先級是:setImmediate->MessageChannel->setTimeout,因此Vue(2.5+)內部的nextTick2.4及以前的實現是不同的,須要注意下。

相關文章
相關標籤/搜索