瀏覽器頁面載入過程

FrancisXu發表於2019-04-23

當使用者輸入網址之後瀏覽器究竟幹了什麼,當然先是域名解析,然後建立連結......網路層面不是我想說的重點,好多前輩大牛都講過,就一筆帶過,著重說的是瀏覽器在拿到HTML、CSS和JS還有image等等素材之後怎麼渲染成我們要看的頁面的(google,百度了好多關鍵詞,很多都把細節講穿了,講得太細了腦海裡沒有整體的概念不太容易整明白,所以拋磚引玉的寫寫)。

1.DNS域名解析

我們在瀏覽器輸入網址,其實就是要向伺服器請求我們想要的頁面內容,所有瀏覽器首先要確認的是域名所對應的伺服器在哪裡。將域名解析成對應的伺服器IP地址這項工作,是由DNS伺服器來完成的。

打個不太恰當的比方,現在註冊賬號都要繫結手機號,網站不一定要輸入使用者名稱,只要有手機號就能註冊然後登入。有的人說我新手機號記不住,可以,你自己搞個記得住的暱稱也可以登入。域名就是你的暱稱,ip地址就是你的手機號,網站必須要有ip地址才能訪問,DNS解析就是通過暱稱找到ip地址然後訪問網站。

客戶端收到你輸入的域名地址後,它首先去找本地的hosts檔案,檢查在該檔案中是否有相應的域名、IP對應關係,如果有,則向其IP地址傳送請求,如果沒有,再去找DNS伺服器。一般使用者很少去編輯修改hosts檔案。

之前流行過一種科學上網方式,就是修改這個檔案的ip對應關係來實現的,不過現在GFW再次加固了,能用的ip越來越少,這種武功祕籍已經絕跡江湖了。

2.建立TCP連線

費了一頓周折終於拿到伺服器IP了,與伺服器建立TCP連線

3.傳送HTTP請求

與伺服器建立了TCP連線後,就可以向伺服器發起請求了。

4.伺服器處理請求

伺服器端收到請求後的由web伺服器處理請求,諸如Apache、Ngnix、IIS等。web伺服器解析使用者請求,知道了需要排程哪些資原始檔,再通過相應的這些資原始檔處理使用者請求和引數,並呼叫資料庫資訊,最後將結果通過web伺服器返回給瀏覽器客戶端。

5.返回響應結果

6.關閉TCP連線

為了避免伺服器與客戶端雙方的資源佔用和損耗,當雙方沒有請求或響應傳遞時,任意一方都可以發起關閉請求。與建立TCP連線的3次握手類似,關閉TCP連線,需要4次握手。

網路階段,完


7.瀏覽器解析HTML

1.構建DOM Tree

當瀏覽器接收到伺服器響應來的HTML文件後,會遍歷文件節點,生成DOM Tree。

遇到<script>標籤的時候,會立即解析指令碼,停止解析文件(因為JS可能會改動DOM和CSS,所以繼續解析會造成浪費)。如果指令碼是外部的,會等待指令碼下載完畢,再繼續解析文件。現在可以在script標籤上增加屬性 defer或者async,強行跳過html渲染完成之後再載入。指令碼解析會將指令碼中改變DOM和CSS的地方分別解析出來,追加到DOM Tree和Style Rules上。 我們可以看到這回有兩個問題,一個是如果程式碼太多,載入編譯執行時間太長,頁面就卡住了,而是程式碼中操作dom元素或者css,如果頁面沒有載入完,可能有些dom獲取不到,剛開始工作時就有一條金科玉律,<script>放在頁面底部。

2.構建CSSOM

瀏覽器的CSS Parser將CSS解析成Style Rules,Style Rules也叫CSSOM(CSS Object Model)。 StyleRules也是一個樹形結構,根據CSS檔案整理出來的類似DOM Tree的樹形結構。CSS的渲染和HTML渲染是同步進行的,所以越快載入出來越好,上面的金科玉律後一半就是css放在頁面頂部。

3.構建Render Tree

DOM Tree和CSSOM我們便可以構建Render Tree。瀏覽器會先從DOM Tree的根節點開始遍歷每個可見節點。對每個可見節點,找到其適配的CSS樣式規則並應用。這中間包括了兩個步驟,樣式計算和樣式覆蓋

樣式計算

Render Tree上每個節點都繫結好了,包括瀏覽器預設樣式表,自定義樣式表,inline樣式元素,HTML視覺化屬性如:width=100。後者將轉化以匹配CSS樣式,需要加以計算。

樣式覆蓋 同樣的標籤可能被重複定義,也可能被泛式的定義所影響,也有可能根本沒有被定義。權重高的定義方式覆蓋權重低的,得到最終呈現的樣式,這一個過程可以通過控制檯看到一些細節。

4.佈局(Layout)

建立渲染樹後,下一步就是佈局(Layout),或者叫回流(reflow,relayout),這個過程就是通過渲染樹中渲染物件的資訊,計算出每一個渲染物件的位置和尺寸,將其安置在瀏覽器視窗的正確位置,而有些時候我們會在文件佈局完成後對DOM進行修改,這時候可能需要重新進行佈局,也可稱其為迴流,本質上還是一個佈局的過程,每一個渲染物件都有一個佈局或者回流方法,實現其佈局或迴流。

通俗的講就是開會排座位,瞭解到有哪些領導要來,把這些領導的位置排排好。

5.繪製(Painting)

在繪製階段,系統會遍歷呈現樹,並呼叫呈現器的“paint”方法,將呈現器的內容顯示在螢幕上。繪製工作是使用使用者介面基礎元件完成的。

上一步是把領導位置訂好了,這一步就是領匯入座(要是把局長排在處長的後面,你這瀏覽器可以不用幹了)。

8.JS的載入

準確說JS的載入是HTML的載入的一部分,一開始網頁載入只有HTML,後來發展出了JS。但是現在JS的發展顯然使其靈活性、可塑性比HTML要高(個人想法),所以想單獨拿出來講。

我們口中的JS其實分為三部分,ECMAScript、DOM、BOM。

習慣把ECMAScript和JS說在一起,其實它是JS最核心的一部分。瀏覽器是ECMAScript的宿主環境之一(nodeJS也是宿主環境)。

DOM是瀏覽器這個宿主環境針對程式開發的擴充套件,也有其他語言可以操作DOM,但是JS的發展最完善,其他方法也就被沉了。

BOM這個可能有些人不瞭解,DOM是文章物件模型,直白點就是瀏覽器HTML這一部分。但是瀏覽器就是一個承載HTML的容器,針對這個容器本身的操作就是BOM瀏覽器物件模型,包括了資訊提示(指突破沙盒的作業系統層面的資訊提示)、獲取顯示器解析度等等功能。當然,BOM這個概念也是H5出來之後才清晰起來,本身這些標準就受制於瀏覽器廠商的實現,BOM目前還沒個譜兒。

講了這麼多鋪墊,開始講JS的載入。

1.建立window物件

window物件也叫全域性執行環境,當頁面產生時就會被建立,所有的全域性變數和函式都屬於window的屬性和方法,而DOM Tree也會對映在window的doucment物件上,我們在JS中針對dom的操作都是通過這個document物件(這個物件的執行效率比較低,這也是促使react和vue誕生的原因)。當關閉網頁或者關閉瀏覽器時,全域性執行環境會被銷燬,包括其內部所有成員都被銷燬。

2.載入檔案

沒啥好講的,載入檔案,完了js引擎分析它的語法與詞法是否合法,如果合法進入預編譯

3.預編譯

解釋名詞,預編譯。

先要說解釋型語言和編譯型語言,二者區分沒有制定標準,看看wiki上的解釋:

編譯型語言是程式碼在執行前編譯器將人類可以理解的語言(程式語言)轉換成機器可以理解的語言。用編譯語言寫成的程式,在執行期的執行速度,通常比用解釋型語言寫的程式快。因為程式在編譯期,已經被預先編譯成機器程式碼,可以直接執行,不用像解釋型語言一樣,還要多一道直譯程式。

解釋型語言會將程式碼一句一句直接執行,不需要像編譯語言一樣,經過編譯器先行編譯為機器程式碼,之後再執行。這種程式語言需要利用直譯器,在執行期,動態將程式碼逐句解釋為機器程式碼,或是已經預先編譯為機器程式碼的的子程式,之後再執行。

許多人認為解釋型語言意味著當遇到程式中行號為xyz時直接將其傳給CPU就能執行;但是事實不是這樣。所有的程式語言都是為人類建立的。他們是人類能夠理解的。你必須將程式語言轉換為機器語言。而什麼時候通過什麼方式轉換,就是二者的區別。

舉個典型的例子,考慮下面這一段程式碼。

for(i=0; i>1000; i++){
    sum += i;
}
複製程式碼

在編譯型語言中sum += i部分在迴圈執行時已經編譯成了機器碼,機器碼將直接執行一千次。

但是在解釋型語言中,他會在執行時將sum += i解釋一千次。所以因為對相同的程式碼進行一千次轉換會造成非常大的效能損耗。

所以兩者區別大概就清楚了,編譯語言編譯時間長,但是執行快,而且不需要瀏覽器這樣的容器就能跑。解釋型語言不要編譯直接跑,但是沒法優化,跑起來慢,而且需要載體。因為現在各種裝置種類太多的問題,反而需要容器的成為優點了,因為瀏覽器替我們解決了程式相容,減少開發時間。

扯遠了,搞編譯語言的大佬們為了改善編譯語言的效率而發展出的即時編譯技術,減少編譯時間(我覺得後端的本地執行就是這樣的,我不懂,純屬yy的)

瀏覽器也不傻,你搞了快速編譯那我也搞個預編譯,要是真的像上面那個例子一樣1000次的迴圈轉換1000次那不是比IE還慢。

在預編譯的過程中,瀏覽器會尋找全域性變數宣告,把它作為window的屬性加入到window物件中,並給變數賦值為'undefined';尋找全域性函式宣告,把它作為window的方法加入到window物件中,並將函式體賦值給他(匿名函式是不參與預編譯的,因為它是變數)。

這個過程涉及到很多優化,這兩點是明顯的能被我們感知到的,我的理解是這個過程中,瀏覽器替變數和函式開闢好了記憶體空間,等著後續的操作。而變數提升作為不合理的地方在ES6中已經解決了,函式提升還存在。

4.解釋執行

執行到變數就賦值,如果變數沒有被定義,也就沒有被預編譯直接賦值,在ES5非嚴格模式下這個變數會成為window的一個屬性,也就是成為全域性變數。string、int這樣的值就是直接把值放在變數的儲存空間裡,object物件就是把指標指向變數的儲存空間。函式執行,就將函式的環境推入一個環境的棧中,執行完成後再彈出,控制權交還給之前的環境。仔細腦補這個過程,JS作用域其實就是這樣的執行流機制實現的。

完事,可能有一些不太嚴謹的說法,我只是拋磚引玉,給大家面試吹牛的時候被問到這個問題可以有一個大概的認知。大牛們看到了錯漏之處不要笑,請指出,感謝。

參考

JavaScript高階程式設計

瀏覽器渲染頁面過程與頁面優化

wiki百科

MDN

相關文章