進程與線程#
對於進程和線程,可以比喻為工廠和工人
- 進程是一個工廠,工廠有它的獨立資源(系統分配的獨立一塊內存)
- 工廠之間相互獨立(進程之間相互獨立)
- 線程是工廠中的工人,多個工人協作完成任務(多個線程在進程中協作完成任務)
- 工廠內有一個或多個工人(一個進程由一個或多個線程組成)
- 工人之間共享空間(同一進程下的各個線程之間共享程序的內存空間)
可以理解為進程是能擁有資源和獨立運行的最小單位,線程是建立在進程的基礎上的一次程序運行單位,一個進程中可以有多個線程,官方術語:
- 進程是 cpu 資源分配的最小單位
- 線程是 cpu 調度的最小單位
不同進程之間也可以通信,不過代價較大。現在一般通用說法裡的單線程與多線程,都是指在一個進程內的單和多。
瀏覽器多進程#
需要理解瀏覽器的三個概念:
- 瀏覽器是多進程的
- 瀏覽器之所以能夠運行,是因為系統給它的進程分配了資源(cpu、內存)
- 每打開一個 Tab 頁,就相當於創建了一個獨立的瀏覽器進程。
瀏覽器多進程介紹#
瀏覽器包含的進程:
- Browser 進程:瀏覽器的主進程(負責協調、主控),數量只有一個,作用有:
- 負責瀏覽器界面顯示,與用戶交互,如前進,後退等
- 負責各個頁面的管理,創建和銷毀其他進程
- 將 Renderer 進程得到的內存中的 Bitmap,繪製到用戶界面上
- 網絡資源的管理,下載等
- 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
- GPU 進程:最多只有一個,用於 3D 繪製等
- Renderer 進程(瀏覽器渲染進程,內部是多線程的):Renderer 進程又指瀏覽器內核,默認每個 Tab 頁面一個進程,互不影響。主要作用為頁面渲染,腳本執行,事件處理等
瀏覽器多進程的優勢:
- 避免單個 page crash 影響整個瀏覽器
- 避免第三方插件 crash 影響整個瀏覽器
- 多進程充分利用多核優勢
- 方便使用沙盒模型隔離插件等進程,提高瀏覽器穩定性
Renderer 進程#
我們需要重點討論的是 Renderer 進程,即瀏覽器渲染進程,該進程包含的主要線程有:
- GUI 渲染線程
- 負責渲染瀏覽器界面,解析 HTML,CSS,構建 DOM 樹和 RenderObject 樹,佈局和繪製等
- 當界面需要重繪(Repaint)或由於某種操作引發回流(reflow)時,該線程就會執行
- 注意,GUI 渲染線程與 JS 引擎線程是互斥的,當 JS 引擎執行時 GUI 線程會被掛起,GUI 更新會被保存在一個隊列中等到 JS 引擎空閒時立即被執行
- JS 引擎線程
- 也稱為 JS 內核(例如 V8 引擎),負責解析 Javascript 腳本,運行代碼
- JS 引擎一直等待著任務隊列中任務的到來,然後加以處理,一個 Tab 頁(renderer 進程)中無論什麼時候都只有一個 JS 線程在運行 JS 程序
- 同樣注意,GUI 渲染線程與 JS 引擎線程是互斥的,所以如果 JS 執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞
- 事件觸發線程
- 歸属于瀏覽器而不是 JS 引擎,用來控制事件循環(可以理解,JS 引擎自己都忙不過來,需要瀏覽器另開線程協助)
- 當 JS 引擎執行代碼塊如 setTimeout 時(也可來自瀏覽器內核的其他線程,如鼠標點擊、AJAX 非同步請求等),會將對應任務添加到事件線程中
- 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待 JS 引擎的處理
- 注意,由於 JS 的單線程關係,所以這些待處理隊列中的事件都得排隊等待 JS 引擎空閒時處理
- 定時觸發器線程
- setInterval 與 setTimeout 所在線程
- 瀏覽器定時計數器並不是由 JavaScript 引擎計數的,因為 JavaScript 引擎是單線程的,如果處於阻塞線程狀態就會影響計時的準確,因此通過單獨線程來計時並觸發定時,計時完畢後,添加到事件隊列中,等待 JS 引擎空閒後執行
- 注意,W3C 在 HTML 標準中規定,規定要求 setTimeout 中低於 4ms 的時間間隔算為 4ms。
- 非同步 http 請求線程
- 在 XMLHttpRequest 在連接後是通過瀏覽器新開一個線程請求
- 將檢測到狀態變更時,如果設置有回調函數,非同步線程就產生狀態變更事件,將這個回調再放入事件隊列中,再由 JavaScript 引擎執行
Browser 進程和 Renderer 進程通信#
然後分析 Browser 進程和 Renderer 進程的通信方式:
- Browser 進程收到用戶請求,首先需要獲取頁面內容(譬如通過網絡下載資源),隨後將該任務通過 RendererHost 接口傳遞給 Renderer 進程
- Renderer 進程的 Renderer 接口收到消息,解釋後交給渲染線程,然後開始渲染
- 渲染線程接收請求,加載網頁並渲染網頁,這其中可能需要 Browser 進程獲取資源和需要 GPU 進程來幫助渲染
- 可能會有 JS 線程操作 DOM(這樣可能會造成回流並重繪)
- 最後 Renderer 進程將結果傳遞給 Browser 進程
- Browser 進程接收到結果並將結果繪製出來
Browser 進程和 Renderer 進程通信
Renderer 進程的多線程#
從上面分析我們知道 Renderer 進程是多線程的,主要包括:GUI 渲染線程、JS 引擎線程、事件觸發線程、定時觸發器線程、非同步 http 請求線程。對於這些線程之間,我們需要理解一些概念。
GUI 渲染線程 與 JS 引擎線程互斥#
上面已經提到,GUI 渲染線程與 JS 引擎線程是互斥的。由於 JavaScript 是可操縱 DOM 的,如果在修改這些元素屬性同時渲染界面(即 JS 線程和 UI 線程同時運行),那麼渲染線程前後獲得的元素數據就可能不一致了。
因此為了防止渲染出現不可預期的結果,瀏覽器設置 GUI 渲染線程與 JS 引擎線程為互斥的關係,當 JS 引擎執行時 GUI 線程會被掛起, GUI 更新則會被保存在一個隊列中等到 JS 引擎線程空閒時立即被執行。
從上述的互斥關係,可以推導出,JS 如果執行時間過長就會阻塞頁面。譬如,假設 JS 引擎正在進行巨量的計算,此時就算 GUI 有更新,也會被保存到隊列中,等待 JS 引擎空閒後執行。然後,由於巨量計算,所以 JS 引擎很可能很久很久後才能空閒,自然會感覺到巨卡無比。所以,要儘量避免 JS 執行時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染加載阻塞的感覺。
Web Worker#
關於 Web Worker 引用 MDN 介紹如下:
Web Worker 為 Web 內容在後台線程中運行腳本提供了一種簡單的方法。線程可以執行任務而不干擾用戶界面。此外,他們可以使用 XMLHttpRequest 執行 I/O (儘管 responseXML 和 channel 屬性總是為空)。一旦創建,一個 worker 可以將消息發送到創建它的 JavaScript 代碼,通過將消息發布到該代碼指定的事件處理程序(反之亦然)。
Web Worker 的作用,就是為 JavaScript 創造多線程環境,允許主線程創建 Worker 線程,將一些任務分配給後者運行。在主線程運行的同時,Worker 線程在後台運行,兩者互不干擾。等到 Worker 線程完成計算任務,再把結果返回給主線程。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 線程負擔了,主線程(通常負責 UI 交互)就會很流暢,不會被阻塞或拖慢。
Worker 線程一旦新建成功,就會始終運行,不會被主線程上的活動(比如用戶點擊按鈕、提交表單)打斷。這樣有利於隨時響應主線程的通信。但是,這也造成了 Worker 比較耗費資源,不應該過度使用,而且一旦使用完畢,就應該關閉。
瀏覽器渲染流程#
瀏覽器內容渲染大概可以劃分成以下幾個步驟:
- 解析 html 建立 dom 樹
- 解析 css 構建 render 樹(將 CSS 代碼解析成樹形的數據結構,然後結合 DOM 合併成 render 樹)
- 佈局 render 樹(Layout/reflow),負責各元素尺寸、位置的計算
- 繪製 render 樹(paint),繪製頁面像素信息
- 瀏覽器會將各層的信息發送給 GPU,GPU 會將各層合成(composite),顯示在屏幕上。
渲染完畢後執行 load 事件,其流程圖如下所示:
瀏覽器渲染流程
load 事件與 DOMContentLoaded 事件#
在比較 load 事件與 DOMContentLoaded 事件執行順序之前,先了解它們各自的觸發時機:
- 當 DOMContentLoaded 事件觸發時,僅當 DOM 加載完成,不包括樣式表,圖片(譬如如果有 async 加載的腳本就不一定完成)
- 當 onload 事件觸發時,頁面上所有的 DOM,樣式表,腳本,圖片都已經加載完成了
綜上可以看出執行順序為 DOMContentLoaded -> load
。
css 加載是否會阻塞 dom 樹渲染?#
在解答這個問題之前需要知道一個重要概念:css 是由單獨的下載線程異步下載的。
然後我們可以得到答案:css 加載不會阻塞 DOM 樹解析(異步加載時 DOM 照常構建),但會阻塞 render 樹渲染(渲染時需等 css 加載完畢,因為 render 樹需要 css 信息)。
這是瀏覽器的一種優化機制,因為加載 css 的時候,可能會修改下面 DOM 節點的樣式, 如果 css 加載不阻塞 render 樹渲染的話,那麼當 css 加載完之後,render 樹可能又得重新重繪或者回流了,這就造成了一些沒有必要的損耗。所以乾脆就先把 DOM 樹的結構先解析完,把可以做的工作做完,然後 css 加載完之後,再根據最終的樣式來渲染 render 樹,這種做法性能方面確實會比較好一點。
普通圖層和複合圖層#
我們在瀏覽器渲染流程第 5 步中提到:瀏覽器會將各層的信息發送給 GPU,GPU 會將各層合成(composite),顯示在屏幕上。這裡涉及到 composite
的概念,瀏覽器渲染的圖層一般包含兩大類:普通圖層以及複合圖層。
首先,普通文檔流內可以理解為一個複合圖層(這裡稱為默認複合層,裡面不管添加多少元素,其實都是在同一個複合圖層中)。absolute 與 fixed 佈局雖然可以脫離普通文檔流,但它仍然屬於默認複合層。
然後,可以通過硬件加速的方式,聲明一個新的複合圖層,它會單獨分配資源,當然也會脫離普通文檔流,這樣一來,不管這個複合圖層中怎麼變化,也不會影響默認複合層裡的回流重繪。
可以這麼理解:GPU 中,各個複合圖層是單獨繪製的,所以互不影響。這也是為什麼某些場景硬件加速效果一级棒。
將 DOM 元素變成複合圖層(硬件加速)的方式有:
translate3d、translateZ
opacity
屬性 / 過渡動畫(需要動畫執行的過程中才會創建合成層,動畫沒有開始或結束後元素還會回到之前的狀態)will-change
屬性,提前告訴瀏覽器要變化,這樣瀏覽器會開始做一些優化工作(這個最好用完後就釋放)<video><iframe><canvas><webgl>
等元素
通過上面分析,可以知道 absolute 與硬件加速的區別:absolute 雖然可以脫離普通文檔流,但是無法脫離默認複合層。所以,就算 absolute 中信息改變時不會改變普通文檔流中 render 樹,但是,瀏覽器最終繪製時是整個複合層繪製的,所以 absolute 中信息的改變,仍然會影響整個複合層的繪製。瀏覽器會重繪它,如果複合層中內容多,absolute 帶來的繪製信息變化過大,資源消耗是非常嚴重的。
而硬件加速直接就是在另一個複合層了,所以它的信息改變不會影響默認複合層,只影響屬於自己的複合層,僅僅是引發最後的合成(輸出視圖)。
雖然硬件加速看起來那麼美妙,但是仍需要謹慎使用。儘量不要大量使用複合圖層,否則由於資源消耗過度,頁面反而會變得更卡。
使用硬件加速時,儘可能的使用 index,防止瀏覽器默認給後續的元素創建複合層渲染。原因是在 webkit CSS3 中,如果元素添加了硬件加速,並且 index 層級比較低,那麼在這個元素的後面其他元素會默認變為複合層渲染,如果處理不當會極大的影響性能。
Event Loop#
從這裡終於講到了本文最核心的部分:JS 的運行機制。先回顧下 Renderer 進程的五大線程:GUI 渲染線程、JS 引擎線程、事件觸發線程、定時觸發器線程、非同步 http 請求線程。
再理解一個概念:
- JS 分為同步任務和非同步任務
- 同步任務都在主線程(JS 引擎線程)上執行,形成一個
執行棧
- 主線程之外,事件觸發線程管理著一個
任務隊列
,只要非同步任務有了運行結果,就在任務隊列之中放置一個事件 - 一旦執行棧中的所有同步任務執行完畢,系統就會讀取任務隊列,將可運行的非同步任務添加到可執行棧中,開始執行
執行棧與任務隊列
可以解釋如下:
- 主線程運行執行棧,棧中代碼執行時調用某些 API(如 ajax 請求)產生事件並添加到任務隊列
- 執行棧中代碼執行完畢,讀取任務隊列中的代碼,如此循環
macrotask 與 microtask#
JS 中分為兩種任務類型:macrotask
和microtask
,即宏任務和微任務,在 ECMAScript 中,macrotask 稱為 task
,microtask 稱為 jobs
。
- macrotask:可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行),由事件觸發線程維護
- 每一個 task 會從頭到尾將這個任務執行完畢,不會執行其它
- 瀏覽器為了能夠使得 JS 內部 task 與 DOM 任務能夠有序的執行,會在一個 task 執行結束後,在下一個 task 執行開始前,對頁面進行重新渲染(
task->渲染->task->...
) - microtask:可以理解是在當前 task 執行結束後立即執行的任務,在當前 task 任務後,下個 task 之前,也在渲染之前,由JS 引擎線程維護
- 所以它的響應速度相比 setTimeout(setTimeout 是 task)會更快,因為無需等渲染
- 在某一個 macrotask 執行完後,就會將在它執行期間產生的所有 microtask 都執行完畢(在渲染前)
macrotask 與 microtask 各有哪些?
- macrotask:主代碼塊,setTimeout,setInterval
- microtask:Promise,process.nextTick(在 node 環境下,process.nextTick 的優先級高於 Promise)
最後總結下 macrotask 與 microtask 的運行機制:
macrotask 與 microtask 的運行機制
- 執行一個宏任務(棧中沒有就從事件隊列中獲取)
- 執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
- 宏任務執行完畢後,立即執行當前微任務隊列中的所有微任務(依次執行)
- 當前宏任務執行完畢,開始檢查渲染,然後 GUI 線程接管渲染
- 渲染完畢後,JS 線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
參考文章: