一、渲染機制
- 對於渲染機制,會從
DOCTYPE
及作用、瀏覽器渲染過程、重排Reflow
、重繪Repaint
和 佈局Layout
這幾個方面。 - 對於
DOCTYPE
及作用,DTD
是文檔類型定義,是一系列的語法規則,用來定義XML
或XHTML
的文件類型。瀏覽器會使用它來判斷文檔類型,決定使用何種協議來解析,以及切換瀏覽器模式。DOCTYPE
是用來聲明文檔類型和DTD
規範的,一個主要的用途是文件的合法性驗證。如果文件代碼不合法,那麼瀏覽器解析時便會出一些差錯。 - 常見的
DOCTYPE
,如下所示:
HTML 5
,<!DOCTYPE html>
HTML 4.01 Strict
,這個DTD
包含所有HTML
元素和屬性,但不包括表象或過時的元素(如font
),框架集是不允許的,<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
HTML 4.01 Transitional
,這個DTD
包含所有HTML
元素和屬性,包括表象或過時的元素(如font
),框架集是不允許的,<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
- 對於瀏覽器渲染過程,
HTML
按照HTML Parser
結合DOM
形成DOM Tree
,Style Sheets
按照CSS Parser
形成Style Rules
,DOM Tree
和Style Rules
形成Render Tree
渲染樹,通過Layout
佈局,Render Tree
計算每個節點的位置大小等信息,通過Painting
繪製根據計算好的信息繪製整個頁面,最後Display
呈現出來。 - 解析
HTML
過程中的問題,自上而下解析HTML
,逐漸構建起DOM tree
,遇到style
、link
標籤,會下載解析樣式表,同時構建CSSOM tree
,不會阻塞html
的解析。但是遇到script
標籤,它會立即下載並執行得到的腳本,會阻塞HTML
的解析。直到腳本里的同步代碼部分(settimeout
等異步操作之外的代碼)執行完之後,再接着解析接下來的HTML
。直到將整個HTML
文檔的最後一個標籤解析完畢,DOM tree
生成完畢。然後CSSOM tree
、render tree
生成,開始渲染。雖然script
的下載、執行均會阻塞HTML
的解析,但DOM tree
的生成是在文檔的最後一個標籤解析完之後才生成,那麼script
標籤的在HTML
中的位置對DOM tree
完全生成的時間應該是沒有任何影響的。那爲什麼有把script
標籤放在</body>
閉合之前的說法?
解答:這是因爲實際上瀏覽器在正式渲染之前,會進行預渲染(first paint,怎麼翻譯的我忘了,反正可以理解爲預渲染)。那麼什麼時候預渲染呢:當瀏覽器認爲DOM tree生成得差不多了的時候。那什麼是差不多呢,解析到body部分的第一個script標籤之前。所以script如果放在head裏,會推遲預渲染; 如果放在body的一開頭,後面的一大堆標籤還沒解析,等於欺騙瀏覽器說已經“差不多了”,也就等於違背了設計預渲染的初衷,會造成整體渲染完畢的推遲。放在body中間也是一樣的道理,所以還是放在最尾巴上比較好。
- 對於重排
Reflow
,DOM
結構中的各個元素都有自己的盒子模型,這些都需要瀏覽器根據各種樣式來計算並根據計算結果將元素放到它該出現的位置,這個過程稱爲reflow
。 - 觸發
reflow
的條件,如下所示:
- 當你增加、刪除、修改
DOM
結點時,會導致Reflow
或Repaint
- 當你移動
DOM
的位置,或是搞個動畫的時候 - 當你修改
CSS
樣式的時候 - 當你
Resize
窗口的時候,但是移動端是沒有這個問題,或是滾動的時候 - 當你修改網頁的默認字體時
- 對於重繪
Repaint
,當各種盒子的時候、大小及其他的屬性,例如顏色、字體大小等都確定下來後,瀏覽器於是便把這些元素都按照各自的特性繪製了一遍,於是頁面的內容出現了,這個過程稱爲repaint
。 - 觸發
repaint
的條件,如下所示:
DOM
改動CSS
改動
二、運行機制
- 對於
JS
的運行機制,分爲JS
的單線程、任務隊列和Event Loop
這幾個方面。 - 對於
JS
的單線程,JavaScript
語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。javaScript
的單線程,與它的用途有關。作爲瀏覽器腳本語言,JavaScript
的主要用途是與用戶互動,以及操作DOM
。這決定了它只能是單線程,否則會帶來很複雜的同步問題。爲了利用多核CPU
的計算能力,HTML5
提出Web Worker
標準,允許JavaScript
腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM
。所以,這個新標準並沒有改變JavaScript
單線程的本質。 - 對於任務隊列,單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。
JS
引擎執行異步代碼而不用等待,是因有爲有 消息隊列和事件循環。消息隊列是消息隊列是一個先進先出的隊列,它裏面存放着各種消息。事件循環是事件循環是指主線程重複從消息隊列中取消息、執行的過程。 - 主線程只會做一件事情,就是從消息隊列裏面取消息、執行消息,再取消息、再執行。當消息隊列爲空時,就會等待直到消息隊列變成非空。而且主線程只有在將當前的消息執行完成後,纔會去取下一個消息。這種機制就叫做事件循環機制,取一個消息並執行的過程叫做一次循環。
JS
中分爲兩種任務類型:macrotask
和microtask
,在ECMAScript
中,microtask
稱爲jobs
,macrotask
可稱爲task
。macrotask
(又稱之爲宏任務),可以理解是每次執行棧執行的代碼就是一個宏任務。microtask
(又稱爲微任務),可以理解是在當前task
執行結束後立即執行的任務。宏任務就是說執行棧裏的每一個被執行的代碼就是一個宏任務,包括一個事件產生的回調執行,會在執行完畢一段代碼之後先對dom
進行一次渲染,然後再執行下一個宏任務。微任務是再宏任務執行完畢之後立即執行的,他在dom
重新渲染之前,微任務的相應速度比宏任務是要快的。- 任務隊列就是等候執行的一系列任務,總結如下:
- 任務隊列又分爲
macro-task
(宏任務)和micro-task
(微任務); macro-task
大概包括:script(整體代碼),setTimeout,setInterval,setImmediate,I/O,UI rendering;
micro-task
大概包括:process.nextTick,Promise,Object.observe(已廢棄),MutationObserver(html5新特性)
setTimeout/Promise
等我們稱之爲任務源。而進入任務隊列的是他們指定的具體執行任務。- 來自不同任務源的任務會進入到不同的任務隊列
- 對於優先級,
micro-task > macro-task
。 - 對於
micro-task
:process.nextTick > Promise.then
- 對於
macro-task
:setTimeout > setImmediate
- 對於
Event Loop
事件循環機制,JS
的運行機制,如下所示:
- 執行棧執行宏任務
- 執行棧沒有任務就去輪詢事件隊列
- 如果執行期間遇到微任務,就添加到微任務隊列
- 一個宏任務執行完畢後會立即執行當前微任務隊列的任務
- 宏任務執行完畢後開始渲染
- 然後開啓下一輪宏任務
- 對於異步任務,常用的有,如下所示:
setTimeout
和setInterval
DOM
事件ES6
中的Promise
三、頁面性能
- 題目:提升頁面性能的方法有哪些。
- 對於提升頁面性能,如下所示:
- 資源壓縮合並,減少
HTTP
請求 - 非核心代碼異步加載,異步加載的方式和異步加載的區別
- 利用瀏覽器緩存,緩存的分類和緩存的原理
- 使用
CDN
- 預解析
DNS
- 對於異步加載,異步加載的方式主要分爲三種,動態腳本加載、
defer
和async
,它們之間的區別,如下所示:
defer
是在HTML
解析完以後才執行,如果是多個,按照加載的順序依次執行async
是在加載完以後立即執行,如果是多個,執行順序和加載順序無關
- 對於瀏覽器緩存,主要分爲強緩存和協商緩存。
- 強緩存,不會向服務器發送請求,直接從緩存中讀取資源,在
hrome
控制檯的Network
選項中可以看到該請求返回200
的狀態碼,並且Size
顯示from disk cache
或from memory cache
。強緩存可以通過設置兩種HTTP Header
實現:Expires
和Cache-Control
,Cache-Conctrol
的優先級比Expires
高。強制緩存就是向瀏覽器緩存查找該請求結果,並根據該結果的緩存規則來決定是否使用該緩存結果的過程,強制緩存的情況主要有三種,如下所示:
- 不存在該緩存結果和緩存標識,強制緩存失效,則直接向服務器發起請求(跟第一次發起請求一致)
- 存在該緩存結果和緩存標識,但是結果已經失效,強制緩存失效,則使用協商緩存
- 存在該緩存結果和緩存標識,且該結果沒有還沒有失效,強制緩存生效,直接返回該結果
Expires
和Cache-Control
,如下所示:
Expires
,緩存過期時間,用來指定資源到期的時間,是服務器端的具體的時間點。Expires=max-age + 請求時間
,需要和Last-modified
結合使用。Expires
是Web
服務器響應消息頭字段,在響應http
請求時告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而無需再次請求。Expires
是HTTP/1
的產物,受限於本地時間,如果修改了本地時間,可能會造成緩存失效。Expires: Wed, 22 Oct 2018 08:41:00 GMT
表示資源會在Wed, 22 Oct 2018 08:41:00 GMT
後過期,需要再次請求。Expires
是HTTP/1.0
控制網頁緩存的字段,其值爲服務器返回該請求的結果緩存的到期時間,即再次發送請求時,如果客戶端的時間小於Expires
的值時,直接使用緩存結果。到了HTTP/1.1
,Expires
已經被Cache-Control
替代,原因在於Expires
控制緩存的原理是使用客戶端的時間與服務端返回的時間做對比,如果客戶端與服務端的時間由於某些原因(時區不同;客戶端和服務端有一方的時間不準確)發生誤差,那麼強制緩存直接失效,那麼強制緩存存在的意義就毫無意義。Cache-Control
,在HTTP/1.1
中,Cache-Control
是最重要的規則,主要用於控制網頁緩存。比如當Cache-Control:max-age=300
時,則代表在這個請求正確返回時間(瀏覽器也會記錄下來)的5
分鐘內再次加載資源,就會命中強緩存。Cache-Control
可以在請求頭或者響應頭中設置,並且可以組合使用多種指令,如下所示:public
:所有內容都將被緩存(客戶端和代理服務器都可緩存)private
:所有內容只有客戶端可以緩存,Cache-Control
的默認取值no-cache
:客戶端緩存內容,但是是否使用緩存則需要經過協商緩存來驗證決定,設置了no-cache
之後,並不是說瀏覽器就不再緩存數據,只是瀏覽器在使用緩存數據時,需要先確認一下數據是否還跟服務器保持一致no-store
:所有內容都不會被緩存,即不使用強制緩存,也不使用協商緩存max-age=xxx (xxx is numeric)
:緩存內容將在xxx
秒後失效max-stale
:能容忍的最大過期時間min-fresh
:能夠容忍的最小新鮮度
Expires
和Cache-Control
兩者對比,區別就在於Expires
是http1.0
的產物,Cache-Control
是http1.1
的產物,兩者同時存在的話,Cache-Control
優先級高於Expires
;在某些不支持HTTP1.1
的環境下,Expires
就會發揮用處。所以Expires
其實是過時的產物,現階段它的存在只是一種兼容性的寫法。強緩存判斷是否緩存的依據來自於是否超出某個時間或者某個時間段,而不關心服務器端文件是否已經更新,這可能會導致加載文件不是服務器端最新的內容,那我們如何獲知服務器端內容是否已經發生了更新呢?此時我們需要用到協商緩存策略。- 協商緩存,協商緩存就是強制緩存失效後,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程,可以通過設置兩種
HTTP Header
實現:Last-Modified
和ETag
,情況如下所示:
- 協商緩存生效,返回
304
和Not Modified
- 協商緩存失效,返回
200
和請求結果
Last-Modified
和If-Modified-Since
,如下所示:
- 瀏覽器在第一次訪問資源時,服務器返回資源的同時,在
response header
中添加Last-Modified
的header
,值是這個資源在服務器上的最後修改時間,瀏覽器接收後緩存文件和header
,如Last-Modified: Fri, 22 Jul 2016 01:47:00 GMT
- 瀏覽器下一次請求這個資源,瀏覽器檢測到有
Last-Modified
這個header
,於是添加If-Modified-Since
這個header
,值就是Last-Modified
中的值;服務器再次收到這個資源請求,會根據If-Modified-Since
中的值與服務器中這個資源的最後修改時間對比,如果沒有變化,返回304
和空的響應體,直接從緩存讀取,如果If-Modified-Since
的時間小於服務器中這個資源的最後修改時間,說明文件有更新,於是返回新的資源文件和200
Last-Modified
存在一些弊端,如果本地打開緩存文件,即使沒有對文件進行修改,但還是會造成Last-Modified
被修改,服務端不能命中緩存導致發送相同的資源。因爲Last-Modified
只能以秒計時,如果在不可感知的時間內修改完成文件,那麼服務端會認爲資源還是命中了,不會返回正確的資源- 根據文件修改時間來決定是否緩存尚有不足,能否可以直接根據文件內容是否修改來決定緩存策略,所以在
HTTP / 1.1
出現了ETag
和If-None-Match
ETag
和If-None-Match
,如下所示:
Etag
是服務器響應請求時,返回當前資源文件的一個唯一標識(由服務器生成),只要資源有變化,Etag
就會重新生成- 瀏覽器在下一次加載資源向服務器發送請求時,會將上一次返回的
Etag
值放到request header
裏的If-None-Match
裏,服務器只需要比較客戶端傳來的If-None-Match
跟自己服務器上該資源的ETag
是否一致,就能很好地判斷資源相對客戶端而言是否被修改過了。如果服務器發現ETag
匹配不上,那麼直接以常規GET 200
回包形式將新的資源(當然也包括了新的ETag
)發給客戶端;如果ETag
是一致的,則直接返回304
知會客戶端直接使用本地緩存即可
Last-Modified
與Etag
的比較,如下所示:
- 在精確度上,
Etag
要優於Last-Modified
,Last-Modified
的時間單位是秒,如果某個文件在1
秒內改變了多次,那麼他們的Last-Modified
其實並沒有體現出來修改,但是Etag
每次都會改變確保了精度;如果是負載均衡的服務器,各個服務器生成的Last-Modified
也有可能不一致 - 在性能上,
Eta
g要遜於Last-Modified
,畢竟Last-Modified
只需要記錄時間,而Etag
需要服務器通過算法來計算出一個hash
值 - 在優先級上,服務器校驗優先考慮
Etag
- 緩存機制,強制緩存優先於協商緩存進行,若強制緩存
(Expires和Cache-Control)
生效則直接使用緩存,若不生效則進行協商緩存(Last-Modified / If-Modified-Since和Etag / If-None-Match)
,協商緩存由服務器決定是否使用緩存,若協商緩存失效,那麼代表該請求的緩存失效,返回200
,重新返回資源和緩存標識,再存入瀏覽器緩存中;生效則返回304
,繼續使用緩存。如果什麼緩存策略都沒設置,那麼瀏覽器會採用一個啓發式的算法,通常會取響應頭中的Date
減去Last-Modified
值的10%
作爲緩存時間。 - 實際場景應用緩存策略,如下所示:
- 頻繁變動的資源,
Cache-Control: no-cache
。對於頻繁變動的資源,首先需要使用Cache-Control: no-cache
使瀏覽器每次都請求服務器,然後配合ETag
或者Last-Modified
來驗證資源是否有效。這樣的做法雖然不能節省請求數量,但是能顯著減少響應數據大小 - 不常變化的資源,
Cache-Control: max-age=31536000
。通常在處理這類資源時,給它們的Cache-Control
配置一個很大的max-age=31536000
(一年),這樣瀏覽器之後請求相同的URL
會命中強制緩存。而爲了解決更新的問題,就需要在文件名(或者路徑)中添加hash
, 版本號等動態字符,之後更改動態字符,從而達到更改引用URL
的目的,讓之前的強制緩存失效
- 用戶行爲對瀏覽器緩存的影響,用戶在瀏覽器如何操作時,會觸發怎樣的緩存策略,如下所示:
- 打開網頁,地址欄輸入地址: 查找
disk cache
中是否有匹配。如有則使用;如沒有則發送網絡請求 - 普通刷新
(F5)
:因爲TAB
並沒有關閉,因此memory cache
是可用的,會被優先使用(如果匹配的話)。其次纔是disk cache
- 強制刷新
(Ctrl + F5)
:瀏覽器不使用緩存,因此發送的請求頭部均帶有Cache-control: no-cache
(爲了兼容,還帶了Pragma: no-cache
),服務器直接返回200
和最新內容
四、錯誤監控
- 對於錯誤監控,分爲前端錯誤的分類、錯誤的捕獲方式和上報錯誤的基本原理。
- 對於前端錯誤的分類,主要是分爲即時運行出現的代碼錯誤和資源加載錯誤。
- 對於錯誤的捕獲方式,如下所示:
- 即時運行錯誤的捕獲方式,有
try...catch
、window.onerror
- 資源加載錯誤,有
object.onerror
、performance.getEntries()
、Error
事件捕獲
- 問題:跨域的
JS
運行錯誤可以捕獲嗎,錯誤提示什麼,應該怎麼處理?
解答:可以捕獲,錯誤信息爲
Script error
,錯誤詳情爲null
,處理方式爲 在script
標籤增加crossorigin
屬性,設置JS
資源響應頭Access-Control-Allow-Origin
- 對於上報錯誤的基本原理,如下所示:
- 採用
Ajax
通信的方式上報 - 利用
Image
對象上報
- 錯誤監控的代碼,如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>錯誤監控</title>
<script type="text/javascript">
window.addEventListener('error', function (e) {
console.log('捕獲', e);
}, false);
</script>
</head>
<body>
<script src="//badu.com/test.js" charset="utf-8"></script>
<script type="text/javascript">
(new Image()).src = 'http://baidu.com/tesjk?r=tksjk';
</script>
</body>
</html>