1.瀏覽器的發展史
1992年,托尼喲翰遜(Tony Johnson)發布了Midas,它允許用戶瀏覽UNIX和VMS網頁上的文檔。
1993年,NCSA發布了Mosaic瀏覽器。Mosaic問世,這是一種可以同時顯示文本和圖像的瀏覽器,一經推出就受到全球用戶的歡迎。
1994年:網景瀏覽器發布,它是由曾經參與開發Mosaic的人共同創建,雖然網景的功能也十分有限,只能顯示簡單的靜態html,沒有js,css,但依然大受歡迎,獲得世界范圍內的成功,并占領了絕大多數市場份額。同年也出現了Opera。
1995年微軟發布了萬惡的IE1.0,IE2.0,自此第一次瀏覽器大戰正式打響。
1996年發布的IE3.0和windows操作系統集成在了一起,而此時網景的市場份額已經達到了86%。IE發行后的4年內,在windows操作系統的幫助下,逐漸取代了網景瀏覽器的領導地位,達到了市場份額的75%。
1999年,它已經占據了瀏覽器市場的99%,前端工程師的噩夢來臨了。然而網景公司并沒有一蹶不振,網景在1998年成立了Mozilla基金會,在該基金會的推動下,網景公司開發了著名的開源項目火狐瀏覽器Firefox來迎擊IE,并在2004年發布了1.0版本,拉開了第二次瀏覽器大戰。
2003年,蘋果發布了Safari瀏覽器,而且該瀏覽器被包含在所有蘋果操作系統中,更重要的是,在2005年蘋果開源了Safari瀏覽器的內核webkit。
2008年,谷歌以蘋果開源項目WebKit作為內核,創建了一個新的項目Chromium,在該項目的基礎上發布了自己的瀏覽器產品Chrome,Chrome發展十分迅速,現在已經成為全球最受歡迎的瀏覽器。由于IE的性能和體驗問題,IE逐漸掉隊。-
2015年,微軟也放棄了IE,推出了基于webkit內核的Edge瀏覽器作為IE的替代品
根據statscounter統計,截止2020年5月份,各個瀏覽器的市場份額下。Chrome已經占據百分之60多的市場份額。再看看國內的瀏覽器。我記得小時候身邊的人的瀏覽器都是被360或者qq瀏覽器統治了,后來才知道,長大后才知道你們都是披著馬甲的IE!!不過近幾年國內瀏覽器的都是IE和chromium雙內核。
2.常見瀏覽器的內核
瀏覽器最重要或者說核心的部分是“Rendering Engine”,可大概譯為“渲染引擎”,不過我們一般習慣將之稱為“瀏覽器內核”。負責對網頁語法的解釋并渲染(顯示)網頁。 所以,通常所謂的瀏覽器內核也就是瀏覽器所采用的[渲染引擎],渲染引擎決定了瀏覽器如何顯示網頁的內容以及頁面的格式信息。
基于safari瀏覽器的WebKit的開源項目
- 蘋果的safari瀏覽器:WebKit
- 谷歌的chrome瀏覽器:Blink
- 微軟的Edge瀏覽器: Blink
- opera瀏覽器:Blink
其他瀏覽器的內核
- IE :Trident
- FireFox: Gecko
3.瀏覽器的結構
這里有一個簡化的瀏覽器結z構圖。我們大致可以簡單的分為用戶界面、瀏覽器引擎、渲染引擎。用戶界面用于展示除標簽頁窗口之外的其他用戶界面內容。渲染引擎負責渲染用戶請求的頁面內容。在用戶界面和渲染引擎之間有個瀏覽器引擎,用于在用戶界面和渲染引擎之間傳遞數據。渲染引擎下面還有很多小的功能模塊,比如負責網絡請求的網絡模塊,用于解析和執行js的js解釋器。還有數據存儲持久層,幫助瀏覽器保存各種數據,比如cookie等等。
1.進程與線程
- 進程就是程序的一次執行過程,計算機的內存會為進程分配獨立的內存空間,當程序執行結束后,所占的內存空間就會被回收。每個進程所分配的內存空間都是獨立的,進程間要相互通信,需要用到IPC通信管道。現在的應用大多都是都進程的,避免了因一個進程的卡死導致整個應用崩潰。
- 線程是cpu可調度的最小單元,進程將任務分成更小的任務分配給線程處理,同一個進程內的線程可以進行數據共享。
那今天的主角瀏覽器也是一個多進程結構。但早期的瀏覽器并不是多進程的結構,而是個單進程結構。一個進程中大概有頁面線程負責頁面渲染和展示等,JavaScript線程執行js代碼,還有其他各種線程。單進程的結構引發了很多的問題。一是不穩定,其中一個線程的卡死可能會導致整個進程出問題,比如你打開多個標簽頁,有一個標簽頁卡死,可能會導致整個瀏覽器無法正常運行,二是不安全,線程之間是可以共享數據,那js線程豈不是可以隨意訪問進程內的數據,三是不流暢,一個進程需要負責太多事情,會導致運行效率的問題。所以為了去解決以上這些問題,現在采用了多進程瀏覽器結構。根據進程功能不同來拆解瀏覽器,我們可以將它們分解為這樣的結構。
其中,瀏覽器進程負責與瀏覽器的其他進程協調工作。你可以把他想成一個工廠里的主管,用來協調各個進程部門。網絡進程負責發起接受網絡請求,GPU進程負責圖形渲染,插件進程負責控制網站使用的所有插件,例如Flash。這里插件并不是指的是Chrome市場里安裝的擴展。Extension 進程渲染器進程用來控制顯示tab標簽內的所有內容,瀏覽器在默認情況下會為每個標簽頁都創建一個進程。
4.瀏覽器的渲染原理
1 當你在地址欄輸入地址時,瀏覽器進程的UI線程會捕捉你的輸入內容,如果訪問的是網址,則UI線程會啟動一個網絡線程來請求DNS進行域名解析接著開始連接服務器獲取數據。如果你的輸入不是網址而是一串關鍵詞,瀏覽器就會知道你是要搜索,于是就會使用你默認配置的搜索引擎來查詢。當網絡線程獲取到數據后,會通過SafeBrowsing來檢查該站點的是否是惡意站點。如果是則會展示個警告頁面,告訴你這個站點有安全問題,瀏覽器會阻止你的訪問。當然你也可以強行繼續訪問。SafeBrowsing是谷歌內部的一套站點安全系統,通過檢測該站點的數據來判斷是否安全。比如通過查看該站點的ip是否在他們的黑名單內。當返回數據準備完畢并且安全校驗通過,網絡線程會通知UI線程,我這準備好了,該你拉。然后UI線程會創建一個渲染器進程來渲染頁面。瀏覽器進程通過IPC管道將數據傳遞給渲染器進程,正式進入渲染流程。
2 此時地址欄的狀態更新,比如histroy更新,現在可以點擊導航欄的后退。渲染器進程收到的數據,也就是html。渲染器進程的核心任務就是把html、js、css、img等資源渲染成用戶可交互的web頁面。渲染器進程的主線程將html進行解析,構造DOM數據結構。DOM文檔對象模型是瀏覽器對頁面在其內部表示形式,是Web程序員可以通過JavaScript與之交互的數據結構和API。HTML首先經過Tokeniser標記化,通過詞法分析,將輸入html內容解析成多個標記,根據識別后的標記進行DOM樹構造, 在 DOM樹構造過程中會創建Document對象,然后以Document為根節點的DOM樹不斷進行修改,向其中添加各種元素。HTML代碼中往往會引入一些額外的資源,比如圖片,css和js腳本等。圖片和css這些資源需要通過網絡下載或者從緩存中直接加載。這些資源不會阻塞html的解析,因為他們不會影響DOM的生成,但當html解析過程中遇到script標簽,將停止html解析流程,轉而去加載解析并且執行js。你可能就會問了?為什么不直接跳過js的加載和執行這一過程,等html解析完后再加載運行js呢?這是因為,瀏覽器不知道js的執行是否會改變當前頁面的html的結構,如果js代碼了調用document.write方法來修改html,那之前的html的解析就沒有任何意義了。這也就是為什么我們一直說要把script標簽要放在合適的位置,或者使用async 或defer屬性來異步加載執行js。
3 在html解析完成后,我們就獲得一個dom tree,但我們還不知道dom tree上每個節點應該長什么樣。主線程需要解析css并確定每個DOM節點的即使你沒有提供自定義的css樣式,瀏覽器也有自己的默認的樣式表,比如h2的字體要比h3的大,具體默認的樣式表可以在這里查看。
4 在知道dom結構和每個節點的樣式后,我們接下來需要知道每個節點需要放在頁面上的哪個位置,也就是節點的坐標,以及該節點需要占用多大的區域。這個階段被稱為layout布局,主線程通過遍歷DOM和計算好的樣式來生成layout tree,layout tree上的每個節點都記錄x,y坐標和邊框尺寸。這里需要注意的一點是DOM Tree和layout tree并不是一一對應的,設置了display:none的節點不會出現在layout tree上,而在before偽類中添加了content值的元素,content的內容會出現在layout tree,不會出現在DOM樹里。這是因為DOM 是通過html解析獲得,并不關心樣式。而layout tree是根據dom tree和計算好的樣式來生成,layout tree是和最后展示在屏幕上的的節點是對應的。好了,現在我們已經知道元素的大小,形狀和位置,這還不夠,我們還需要做什么了呢。對了,還需要知道以什么樣的順序繪制各個節點。舉例來說,z-index這個屬性會影響節點繪制的層級關系。如果我們按照dom的層級結構來繪制頁面,則會導致錯誤的渲染。所以為了保證在屏幕上展示正確的層級,在繪制階段,主線程遍歷layout tree創建一個繪制記錄表,該表記錄了繪制的順序。
5 現在知道了文檔的繪制順序,終于到了該把這些信息轉化成像素點顯示在屏幕的時候了。那這種行為,被稱為rasterizing,柵格化。Chrome最早使用了一種很簡單的方式,只柵格化用戶可視區域的內容,當用戶滾動頁面時,再柵格化更多的內容來填充缺失的部分。這種方式帶來的問題顯而易見,會導致展示延遲。隨著不斷的優化升級,現在的Chrome使用了一種更復雜的柵格化流程,叫做compositing組合。Compositing是一種將頁面的各個部分分成多個圖層,分別對其進行柵格化并在合成器線程compositor thread的單獨線程中進行合成頁面的技術。簡單來說就是,頁面所有的元素按照某種規則進行分圖層,并把圖層都柵格化好了,然后只需要把可視區的內容組合成一幀展示給用戶即可。主線程遍歷layout tree生成layer tree。當layer tree生成完畢和繪制順序確定后,主線程將這些信息傳遞給compositor線程。合成器線程將每個圖層柵格化。一層可能像頁面的整個長度一樣大,因此合成器線程將它們切分為多個圖塊,然后將每個圖塊發送給柵格線程。柵格線程柵格化每個圖塊并將它們存儲在GPU內存中。對圖塊進行柵格化后。合成器線程可以給不同的柵格線程分別優先級,比如柵格化可視區域圖塊的柵格線程優先處理。當圖塊柵格化完成后,合成器線程將收集稱為“draw quads”的圖塊信息,這些信息里記錄了包含諸如圖塊在內存中的位置和在頁面的哪個位置繪制圖塊的信息。根據這些數據合成器線程生成了一個合成器Frame。然后這個合成器frame通過IPC傳送給瀏覽器進程,接著瀏覽器進程將compositor frame傳到GPU,然后GPU渲染展示到屏幕上。恭喜你,你終于看到了頁面內容。當你的頁面然后變化,比如你滾動了當前頁面,則會生成一個新的compositor frame,新的frame再傳給GPU。再次渲染到屏幕上。
6.總結: 瀏覽器進程的網絡線程請求獲取到html數據和通過IPC將數據傳給渲染器進程的主線程,主線程講html解析構造DOM樹,然后計算樣式,根據DOM樹和樣式生成layout Tree,通過遍歷layout tree生成繪制順序表,然后主線程將layout Tree和繪制順序信息一起傳給合成器線程,合成器線程按規則進程分圖層,并把圖層分為更小的圖塊傳給柵格線程進行柵格化,柵格化完成后,合成器線程會獲得柵格線程傳過來的"draw quads"圖塊信息,根據這些信息,合成器線程合成了一個frame,然后將該合成frame通過IPC傳回給瀏覽器進程,瀏覽器進程在傳到GPU進行渲染,最后就展示到你的屏幕上了。
當我們改變一個尺寸位置屬性時,會重新進行樣式計算,布局,繪制,以及后面的所有流程。這種行為我們稱為重排。當我們改變某個元素的顏色屬性時,不會重新觸發布局,但還是觸發會樣式計算和繪制,這個就是重繪。我們可以發現重排和重繪會占用主線程,還有一個東西的運行也是在主線程。對,js。既然他們都是在主線程就會出現搶占執行時間的問題。如果你們寫了個不斷導致重繪重排的動畫,瀏覽器則需要在每一幀都會運行樣式計算、布局和繪制的操作,我們知道當頁面以每秒大于60幀的刷新率,才不會讓用戶感覺到頁面卡頓。如果你在運行動畫時,還有大量的js任務需要執行,因為布局繪制和js的執行都是在主線程運行的,當在一幀的時間內,布局和繪制結束后,還有剩余時間,js就會拿到主線程的使用權,如果js執行時間過長就會導致在下一幀開始時,js沒有及時歸還主線程,導致下一幀動畫沒有按時渲染,就會出現頁面動畫的卡頓。那有什么優化的手段嗎?有,第一種就是可以通過requestAnimationFrame這個api來幫助我們解決這個問題。requestAnimationFrame這個方法會在每一幀被調用,通過這個api的回調參數,我們可以知道每一幀當前還剩余的,我們可以把js運行任務分成一些小塊,在時間用完前,歸還主線程。還有第二個優化方法。剛才我們知道柵格化整個流程是不占用主線程的,只在合成器和柵格線程中運行,這就意味著它無需和js搶奪的主線程。剛才提到,如果我們反復重繪和重排,可能會導致掉幀,因為有可能會有js的執行阻塞了主線程。css中有個動畫屬性叫transform,通過該屬性實現的動畫,不會經過布局和繪制,而是直接運行在Compositor和rasterizing線程中,所以不會受到主線程中js執行的影響。更重要的是transform的動畫,由于不需要經過布局繪制樣式計算,所以節省了很多運算時間。可以讓復雜的動畫更加流暢。我們常常會哪些屬性來實現動畫效果呢,位置變化,寬高變化,那這些都可以使用transform來代替。所以說一個頁面的動畫好壞,可以說是十分影響用戶的體驗。