1.瀏覽器的發(fā)展史
1992年午磁,托尼喲翰遜(Tony Johnson)發(fā)布了Midas土全,它允許用戶瀏覽UNIX和VMS網(wǎng)頁上的文檔。
1993年叼丑,NCSA發(fā)布了Mosaic瀏覽器昔案。Mosaic問世瓶堕,這是一種可以同時顯示文本和圖像的瀏覽器颠印,一經(jīng)推出就受到全球用戶的歡迎昧诱。
1994年:網(wǎng)景瀏覽器發(fā)布畔塔,它是由曾經(jīng)參與開發(fā)Mosaic的人共同創(chuàng)建潭辈,雖然網(wǎng)景的功能也十分有限,只能顯示簡單的靜態(tài)html澈吨,沒有js把敢,css,但依然大受歡迎谅辣,獲得世界范圍內(nèi)的成功修赞,并占領了絕大多數(shù)市場份額。同年也出現(xiàn)了Opera。
1995年微軟發(fā)布了萬惡的IE1.0,IE2.0柏副,自此第一次瀏覽器大戰(zhàn)正式打響勾邦。
1996年發(fā)布的IE3.0和windows操作系統(tǒng)集成在了一起,而此時網(wǎng)景的市場份額已經(jīng)達到了86%割择。IE發(fā)行后的4年內(nèi)眷篇,在windows操作系統(tǒng)的幫助下,逐漸取代了網(wǎng)景瀏覽器的領導地位荔泳,達到了市場份額的75%蕉饼。
1999年,它已經(jīng)占據(jù)了瀏覽器市場的99%玛歌,前端工程師的噩夢來臨了昧港。然而網(wǎng)景公司并沒有一蹶不振,網(wǎng)景在1998年成立了Mozilla基金會支子,在該基金會的推動下创肥,網(wǎng)景公司開發(fā)了著名的開源項目火狐瀏覽器Firefox來迎擊IE,并在2004年發(fā)布了1.0版本译荞,拉開了第二次瀏覽器大戰(zhàn)瓤的。
2003年,蘋果發(fā)布了Safari瀏覽器吞歼,而且該瀏覽器被包含在所有蘋果操作系統(tǒng)中圈膏,更重要的是,在2005年蘋果開源了Safari瀏覽器的內(nèi)核webkit篙骡。
2008年稽坤,谷歌以蘋果開源項目WebKit作為內(nèi)核,創(chuàng)建了一個新的項目Chromium糯俗,在該項目的基礎上發(fā)布了自己的瀏覽器產(chǎn)品Chrome尿褪,Chrome發(fā)展十分迅速,現(xiàn)在已經(jīng)成為全球最受歡迎的瀏覽器得湘。由于IE的性能和體驗問題杖玲,IE逐漸掉隊。-
2015年淘正,微軟也放棄了IE摆马,推出了基于webkit內(nèi)核的Edge瀏覽器作為IE的替代品
根據(jù)statscounter統(tǒng)計,截止2020年5月份鸿吆,各個瀏覽器的市場份額下囤采。Chrome已經(jīng)占據(jù)百分之60多的市場份額。再看看國內(nèi)的瀏覽器惩淳。我記得小時候身邊的人的瀏覽器都是被360或者qq瀏覽器統(tǒng)治了蕉毯,后來才知道,長大后才知道你們都是披著馬甲的IE!代虾!不過近幾年國內(nèi)瀏覽器的都是IE和chromium雙內(nèi)核进肯。
2.常見瀏覽器的內(nèi)核
瀏覽器最重要或者說核心的部分是“Rendering Engine”,可大概譯為“渲染引擎”棉磨,不過我們一般習慣將之稱為“瀏覽器內(nèi)核”坷澡。負責對網(wǎng)頁語法的解釋并渲染(顯示)網(wǎng)頁。 所以含蓉,通常所謂的瀏覽器內(nèi)核也就是瀏覽器所采用的[渲染引擎],渲染引擎決定了瀏覽器如何顯示網(wǎng)頁的內(nèi)容以及頁面的格式信息项郊。
基于safari瀏覽器的WebKit的開源項目
- 蘋果的safari瀏覽器:WebKit
- 谷歌的chrome瀏覽器:Blink
- 微軟的Edge瀏覽器: Blink
- opera瀏覽器:Blink
其他瀏覽器的內(nèi)核
- IE :Trident
- FireFox: Gecko
3.瀏覽器的結(jié)構(gòu)
這里有一個簡化的瀏覽器結(jié)z構(gòu)圖馅扣。我們大致可以簡單的分為用戶界面、瀏覽器引擎着降、渲染引擎差油。用戶界面用于展示除標簽頁窗口之外的其他用戶界面內(nèi)容。渲染引擎負責渲染用戶請求的頁面內(nèi)容任洞。在用戶界面和渲染引擎之間有個瀏覽器引擎蓄喇,用于在用戶界面和渲染引擎之間傳遞數(shù)據(jù)。渲染引擎下面還有很多小的功能模塊交掏,比如負責網(wǎng)絡請求的網(wǎng)絡模塊妆偏,用于解析和執(zhí)行js的js解釋器。還有數(shù)據(jù)存儲持久層盅弛,幫助瀏覽器保存各種數(shù)據(jù)钱骂,比如cookie等等。
1.進程與線程
- 進程就是程序的一次執(zhí)行過程挪鹏,計算機的內(nèi)存會為進程分配獨立的內(nèi)存空間见秽,當程序執(zhí)行結(jié)束后,所占的內(nèi)存空間就會被回收讨盒。每個進程所分配的內(nèi)存空間都是獨立的解取,進程間要相互通信,需要用到IPC通信管道》邓常現(xiàn)在的應用大多都是都進程的禀苦,避免了因一個進程的卡死導致整個應用崩潰。
- 線程是cpu可調(diào)度的最小單元创南,進程將任務分成更小的任務分配給線程處理伦忠,同一個進程內(nèi)的線程可以進行數(shù)據(jù)共享。
那今天的主角瀏覽器也是一個多進程結(jié)構(gòu)稿辙。但早期的瀏覽器并不是多進程的結(jié)構(gòu)昆码,而是個單進程結(jié)構(gòu)。一個進程中大概有頁面線程負責頁面渲染和展示等,JavaScript線程執(zhí)行js代碼赋咽,還有其他各種線程旧噪。單進程的結(jié)構(gòu)引發(fā)了很多的問題。一是不穩(wěn)定脓匿,其中一個線程的卡死可能會導致整個進程出問題淘钟,比如你打開多個標簽頁,有一個標簽頁卡死陪毡,可能會導致整個瀏覽器無法正常運行米母,二是不安全,線程之間是可以共享數(shù)據(jù)毡琉,那js線程豈不是可以隨意訪問進程內(nèi)的數(shù)據(jù)铁瞒,三是不流暢,一個進程需要負責太多事情桅滋,會導致運行效率的問題慧耍。所以為了去解決以上這些問題,現(xiàn)在采用了多進程瀏覽器結(jié)構(gòu)丐谋。根據(jù)進程功能不同來拆解瀏覽器芍碧,我們可以將它們分解為這樣的結(jié)構(gòu)。
其中号俐,瀏覽器進程負責與瀏覽器的其他進程協(xié)調(diào)工作泌豆。你可以把他想成一個工廠里的主管,用來協(xié)調(diào)各個進程部門吏饿。網(wǎng)絡進程負責發(fā)起接受網(wǎng)絡請求践美,GPU進程負責圖形渲染,插件進程負責控制網(wǎng)站使用的所有插件找岖,例如Flash陨倡。這里插件并不是指的是Chrome市場里安裝的擴展。Extension 進程渲染器進程用來控制顯示tab標簽內(nèi)的所有內(nèi)容许布,瀏覽器在默認情況下會為每個標簽頁都創(chuàng)建一個進程兴革。
4.瀏覽器的渲染原理
1 當你在地址欄輸入地址時,瀏覽器進程的UI線程會捕捉你的輸入內(nèi)容蜜唾,如果訪問的是網(wǎng)址杂曲,則UI線程會啟動一個網(wǎng)絡線程來請求DNS進行域名解析接著開始連接服務器獲取數(shù)據(jù)。如果你的輸入不是網(wǎng)址而是一串關鍵詞袁余,瀏覽器就會知道你是要搜索擎勘,于是就會使用你默認配置的搜索引擎來查詢。當網(wǎng)絡線程獲取到數(shù)據(jù)后颖榜,會通過SafeBrowsing來檢查該站點的是否是惡意站點棚饵。如果是則會展示個警告頁面煤裙,告訴你這個站點有安全問題,瀏覽器會阻止你的訪問噪漾。當然你也可以強行繼續(xù)訪問硼砰。SafeBrowsing是谷歌內(nèi)部的一套站點安全系統(tǒng),通過檢測該站點的數(shù)據(jù)來判斷是否安全欣硼。比如通過查看該站點的ip是否在他們的黑名單內(nèi)题翰。當返回數(shù)據(jù)準備完畢并且安全校驗通過,網(wǎng)絡線程會通知UI線程诈胜,我這準備好了豹障,該你拉。然后UI線程會創(chuàng)建一個渲染器進程來渲染頁面焦匈。瀏覽器進程通過IPC管道將數(shù)據(jù)傳遞給渲染器進程沼填,正式進入渲染流程。
2 此時地址欄的狀態(tài)更新括授,比如histroy更新,現(xiàn)在可以點擊導航欄的后退岩饼。渲染器進程收到的數(shù)據(jù)荚虚,也就是html。渲染器進程的核心任務就是把html籍茧、js版述、css、img等資源渲染成用戶可交互的web頁面寞冯。渲染器進程的主線程將html進行解析渴析,構(gòu)造DOM數(shù)據(jù)結(jié)構(gòu)。DOM文檔對象模型是瀏覽器對頁面在其內(nèi)部表示形式吮龄,是Web程序員可以通過JavaScript與之交互的數(shù)據(jù)結(jié)構(gòu)和API俭茧。HTML首先經(jīng)過Tokeniser標記化,通過詞法分析漓帚,將輸入html內(nèi)容解析成多個標記母债,根據(jù)識別后的標記進行DOM樹構(gòu)造, 在 DOM樹構(gòu)造過程中會創(chuàng)建Document對象,然后以Document為根節(jié)點的DOM樹不斷進行修改尝抖,向其中添加各種元素毡们。HTML代碼中往往會引入一些額外的資源,比如圖片昧辽,css和js腳本等衙熔。圖片和css這些資源需要通過網(wǎng)絡下載或者從緩存中直接加載。這些資源不會阻塞html的解析搅荞,因為他們不會影響DOM的生成红氯,但當html解析過程中遇到script標簽框咙,將停止html解析流程,轉(zhuǎn)而去加載解析并且執(zhí)行js脖隶。你可能就會問了扁耐?為什么不直接跳過js的加載和執(zhí)行這一過程,等html解析完后再加載運行js呢产阱?這是因為婉称,瀏覽器不知道js的執(zhí)行是否會改變當前頁面的html的結(jié)構(gòu),如果js代碼了調(diào)用document.write方法來修改html构蹬,那之前的html的解析就沒有任何意義了王暗。這也就是為什么我們一直說要把script標簽要放在合適的位置,或者使用async 或defer屬性來異步加載執(zhí)行js庄敛。
3 在html解析完成后俗壹,我們就獲得一個dom tree,但我們還不知道dom tree上每個節(jié)點應該長什么樣藻烤。主線程需要解析css并確定每個DOM節(jié)點的即使你沒有提供自定義的css樣式绷雏,瀏覽器也有自己的默認的樣式表,比如h2的字體要比h3的大怖亭,具體默認的樣式表可以在這里查看涎显。
4 在知道dom結(jié)構(gòu)和每個節(jié)點的樣式后,我們接下來需要知道每個節(jié)點需要放在頁面上的哪個位置兴猩,也就是節(jié)點的坐標期吓,以及該節(jié)點需要占用多大的區(qū)域。這個階段被稱為layout布局倾芝,主線程通過遍歷DOM和計算好的樣式來生成layout tree讨勤,layout tree上的每個節(jié)點都記錄x,y坐標和邊框尺寸。這里需要注意的一點是DOM Tree和layout tree并不是一一對應的晨另,設置了display:none的節(jié)點不會出現(xiàn)在layout tree上潭千,而在before偽類中添加了content值的元素,content的內(nèi)容會出現(xiàn)在layout tree,不會出現(xiàn)在DOM樹里借尿。這是因為DOM 是通過html解析獲得脊岳,并不關心樣式。而layout tree是根據(jù)dom tree和計算好的樣式來生成垛玻,layout tree是和最后展示在屏幕上的的節(jié)點是對應的割捅。好了,現(xiàn)在我們已經(jīng)知道元素的大小帚桩,形狀和位置亿驾,這還不夠,我們還需要做什么了呢账嚎。對了莫瞬,還需要知道以什么樣的順序繪制各個節(jié)點儡蔓。舉例來說,z-index這個屬性會影響節(jié)點繪制的層級關系疼邀。如果我們按照dom的層級結(jié)構(gòu)來繪制頁面喂江,則會導致錯誤的渲染。所以為了保證在屏幕上展示正確的層級旁振,在繪制階段获询,主線程遍歷layout tree創(chuàng)建一個繪制記錄表,該表記錄了繪制的順序拐袜。
5 現(xiàn)在知道了文檔的繪制順序吉嚣,終于到了該把這些信息轉(zhuǎn)化成像素點顯示在屏幕的時候了。那這種行為蹬铺,被稱為rasterizing,柵格化尝哆。Chrome最早使用了一種很簡單的方式,只柵格化用戶可視區(qū)域的內(nèi)容甜攀,當用戶滾動頁面時秋泄,再柵格化更多的內(nèi)容來填充缺失的部分。這種方式帶來的問題顯而易見规阀,會導致展示延遲恒序。隨著不斷的優(yōu)化升級,現(xiàn)在的Chrome使用了一種更復雜的柵格化流程姥敛,叫做compositing組合。Compositing是一種將頁面的各個部分分成多個圖層瞎暑,分別對其進行柵格化并在合成器線程compositor thread的單獨線程中進行合成頁面的技術(shù)彤敛。簡單來說就是,頁面所有的元素按照某種規(guī)則進行分圖層了赌,并把圖層都柵格化好了墨榄,然后只需要把可視區(qū)的內(nèi)容組合成一幀展示給用戶即可。主線程遍歷layout tree生成layer tree勿她。當layer tree生成完畢和繪制順序確定后袄秩,主線程將這些信息傳遞給compositor線程。合成器線程將每個圖層柵格化逢并。一層可能像頁面的整個長度一樣大之剧,因此合成器線程將它們切分為多個圖塊,然后將每個圖塊發(fā)送給柵格線程砍聊。柵格線程柵格化每個圖塊并將它們存儲在GPU內(nèi)存中背稼。對圖塊進行柵格化后。合成器線程可以給不同的柵格線程分別優(yōu)先級玻蝌,比如柵格化可視區(qū)域圖塊的柵格線程優(yōu)先處理蟹肘。當圖塊柵格化完成后词疼,合成器線程將收集稱為“draw quads”的圖塊信息,這些信息里記錄了包含諸如圖塊在內(nèi)存中的位置和在頁面的哪個位置繪制圖塊的信息帘腹。根據(jù)這些數(shù)據(jù)合成器線程生成了一個合成器Frame贰盗。然后這個合成器frame通過IPC傳送給瀏覽器進程,接著瀏覽器進程將compositor frame傳到GPU阳欲,然后GPU渲染展示到屏幕上舵盈。恭喜你,你終于看到了頁面內(nèi)容胸完。當你的頁面然后變化书释,比如你滾動了當前頁面,則會生成一個新的compositor frame赊窥,新的frame再傳給GPU爆惧。再次渲染到屏幕上。
6.總結(jié): 瀏覽器進程的網(wǎng)絡線程請求獲取到html數(shù)據(jù)和通過IPC將數(shù)據(jù)傳給渲染器進程的主線程锨能,主線程講html解析構(gòu)造DOM樹扯再,然后計算樣式,根據(jù)DOM樹和樣式生成layout Tree址遇,通過遍歷layout tree生成繪制順序表熄阻,然后主線程將layout Tree和繪制順序信息一起傳給合成器線程,合成器線程按規(guī)則進程分圖層倔约,并把圖層分為更小的圖塊傳給柵格線程進行柵格化秃殉,柵格化完成后,合成器線程會獲得柵格線程傳過來的"draw quads"圖塊信息浸剩,根據(jù)這些信息钾军,合成器線程合成了一個frame,然后將該合成frame通過IPC傳回給瀏覽器進程绢要,瀏覽器進程在傳到GPU進行渲染吏恭,最后就展示到你的屏幕上了。
當我們改變一個尺寸位置屬性時重罪,會重新進行樣式計算樱哼,布局,繪制剿配,以及后面的所有流程搅幅。這種行為我們稱為重排。當我們改變某個元素的顏色屬性時呼胚,不會重新觸發(fā)布局盏筐,但還是觸發(fā)會樣式計算和繪制,這個就是重繪砸讳。我們可以發(fā)現(xiàn)重排和重繪會占用主線程琢融,還有一個東西的運行也是在主線程界牡。對,js漾抬。既然他們都是在主線程就會出現(xiàn)搶占執(zhí)行時間的問題宿亡。如果你們寫了個不斷導致重繪重排的動畫,瀏覽器則需要在每一幀都會運行樣式計算纳令、布局和繪制的操作挽荠,我們知道當頁面以每秒大于60幀的刷新率,才不會讓用戶感覺到頁面卡頓平绩。如果你在運行動畫時圈匆,還有大量的js任務需要執(zhí)行,因為布局繪制和js的執(zhí)行都是在主線程運行的捏雌,當在一幀的時間內(nèi)跃赚,布局和繪制結(jié)束后,還有剩余時間性湿,js就會拿到主線程的使用權(quán)纬傲,如果js執(zhí)行時間過長就會導致在下一幀開始時,js沒有及時歸還主線程肤频,導致下一幀動畫沒有按時渲染叹括,就會出現(xiàn)頁面動畫的卡頓。那有什么優(yōu)化的手段嗎宵荒?有汁雷,第一種就是可以通過requestAnimationFrame這個api來幫助我們解決這個問題。requestAnimationFrame這個方法會在每一幀被調(diào)用报咳,通過這個api的回調(diào)參數(shù)侠讯,我們可以知道每一幀當前還剩余的,我們可以把js運行任務分成一些小塊少孝,在時間用完前继低,歸還主線程熬苍。還有第二個優(yōu)化方法稍走。剛才我們知道柵格化整個流程是不占用主線程的,只在合成器和柵格線程中運行柴底,這就意味著它無需和js搶奪的主線程婿脸。剛才提到,如果我們反復重繪和重排柄驻,可能會導致掉幀狐树,因為有可能會有js的執(zhí)行阻塞了主線程。css中有個動畫屬性叫transform鸿脓,通過該屬性實現(xiàn)的動畫抑钟,不會經(jīng)過布局和繪制涯曲,而是直接運行在Compositor和rasterizing線程中,所以不會受到主線程中js執(zhí)行的影響在塔。更重要的是transform的動畫幻件,由于不需要經(jīng)過布局繪制樣式計算,所以節(jié)省了很多運算時間蛔溃〈铝ぃ可以讓復雜的動畫更加流暢。我們常常會哪些屬性來實現(xiàn)動畫效果呢贺待,位置變化徽曲,寬高變化,那這些都可以使用transform來代替麸塞。所以說一個頁面的動畫好壞秃臣,可以說是十分影響用戶的體驗。