效能優化是一門大學問,本文僅對個人一些積累知識的闡述,歡迎下面補充。
丟擲一個問題,從輸入
url
位址列到所有內容顯示到介面上做了哪些事?
- 1.瀏覽器向
DNS
伺服器請求解析該 URL 中的域名所對應的IP
地址; - 2.建立
TCP
連線(三次握手); - 3.瀏覽器發出讀取檔案(
URL
中域名後面部分對應的檔案)的HTTP
請求,該請求報文作為TCP
三次握手的第三個報文的資料傳送給伺服器; - 4.伺服器對瀏覽器請求作出響應,並把對應的 html 文字傳送給瀏覽器;
- 5.瀏覽器將該
html
文字並顯示內容; - 6.釋放
TCP
連線(四次揮手);
上面這個問題是一個面試官非常喜歡問的問題,我們下面把這6個步驟分解,逐步細談優化。
一、DNS
解析
-
DNS`解析:將域名解析為ip地址 ,由上往下匹配,只要命中便停止
- 走快取 - 瀏覽器DNS快取 - 本機DNS快取 - 路由器DNS快取 - 網路運營商伺服器DNS快取 (80%的DNS解析在這完成的) - 遞迴查詢 複製程式碼
優化策略:儘量允許使用瀏覽器的快取,能給我們節省大量時間。
二、TCP
的三次握手
-
SYN (同步序列編號)ACK(確認字元)
-
第一次握手:Client將標誌位SYN置為1,隨機產生一個值seq=J,並將該資料包傳送給Server,Client進入SYN_SENT狀態,等 待Server確認。
-
第二次握手:Server收到資料包後由標誌位SYN=1知道Client請求建立連線,Server將標誌位SYN和ACK都置為1,ack=J+1,隨機產生一個值seq=K,並將該資料包傳送給Client以確認連線請求,Server進入SYN_RCVD狀態。
-
第三次握手:Client收到確認後,檢查ack是否為J+1,ACK是否為1,如果正確則將標誌位ACK置為1,ack=K+1,並將該資料包傳送給Server,Server檢查ack是否為K+1,ACK是否為1,如果正確則連線建立成功,Client和Server進入ESTABLISHED狀態,完成三次握手,隨後Client與Server之間可以開始傳輸資料了。
三、瀏覽器傳送請求
優化策略:
- 1.
HTTP
協議通訊最耗費時間的是建立TCP
連線的過程,那我們就可以使用HTTP Keep-Alive
,在HTTP
早期,每個HTTP
請求都要求開啟一個TCP socket
連線,並且使用一次之後就斷開這個TCP
連線。 使用keep-alive
可以改善這種狀態,即在一次TCP連線中可以持續傳送多份資料而不會斷開連線。通過使用keep-alive
機制,可以減少TCP
連線建立次數,也意味著可以減少TIME_WAIT
狀態連線,以此提高效能和提高http
伺服器的吞吐率(更少的tcp
連線意味著更少的系統核心呼叫 - 2.但是,
keep-alive
並不是免費的午餐,長時間的TCP
連線容易導致系統資源無效佔用。配置不當的keep-alive
,有時比重複利用連線帶來的損失還更大。所以,正確地設定keep-alive timeout
時間非常重要。(這個keep-alive_timout
時間值意味著:一個http
產生的tcp
連線在傳送完最後一個響應後,還需要hold
住keepalive_timeout
秒後,才開始關閉這個連線),如果想更詳細瞭解可以看這篇文章keep-alve效能優化的測試結果 - 3.使用
webScoket
通訊協議,僅一次TCP
握手就一直保持連線,而且他對二進位制資料的傳輸有更好的支援,可以應用於即時通訊,海量高併發場景。webSocket的原理以及詳解 - 4.減少
HTTP
請求次數,每次HTTP
請求都會有請求頭,返回響應都會有響應頭,多次請求不僅浪費時間而且會讓網路傳輸很多無效的資源,使用前端模組化技術AMD CMD commonJS ES6等模組化方案
將多個檔案壓縮打包成一個,當然也不能都放在一個檔案中,因為這樣傳輸起來可能會很慢,權衡取一箇中間值 - 5.配置使用懶載入,對於一些使用者不立刻使用到的檔案到特定的事件觸發再請求,也許使用者只是想看到你首頁上半屏的內容,但是你卻請求了整個頁面的所有圖片,如果使用者量很大,那麼這是一種極大的浪費
- 6.伺服器資源的部署儘量使用同源策略
四、伺服器返回響應,瀏覽器接受到響應資料
五、瀏覽器解析資料,繪製渲染頁面的過程
- 先預解析(將需要傳送請求的標籤的請求發出去)
- 從上到下解析
html
檔案 - 遇到HTML標籤,呼叫html解析器將其解析
DOM
樹 - 遇到
css
標記,呼叫css解析器將其解析CSSOM
樹 link
阻塞 - 為了解決閃屏,所有解決閃屏的樣式style
非阻塞,與閃屏的樣式不相關的- 將
DOM
樹和CSSOM
樹結合在一起,形成render
樹 - layout佈局 render渲染
- 遇到
script
標籤,阻塞,呼叫js
解析器解析js
程式碼,可能會修改DOM
樹,也可能會修改CSSOM
樹 - 將
DOM
樹和CSSOM
樹結合在一起,形成render
樹 layout
佈局render
渲染(重排重繪)script
標籤的屬性- async 非同步 誰先回來誰就先解析,不阻塞
- defer 非同步 按照先後順序(defer)解析,不阻塞
- script標籤放在body下,放置多次重排重繪,能夠操作dom
效能優化策略:
- 需要阻塞的樣式使用
link
引入,不需要的使用style
標籤(具體是否需要阻塞看業務場景) - 圖片比較多的時候,一定要使用懶載入,圖片是最需要優化的,
webpack4
中也要配置圖片壓縮,能極大壓縮圖片大小,對於新版本瀏覽器可以使用webp格式圖片
webP詳解,圖片優化對效能提升最大。 webpack4
配置 程式碼分割,提取公共程式碼成單獨模組。方便快取
/*
runtimeChunk 設定為 true, webpack 就會把 chunk 檔名全部存到一個單獨的 chunk 中,
這樣更新一個檔案只會影響到它所在的 chunk 和 runtimeChunk,避免了引用這個 chunk 的檔案也發生改變。
*/
runtimeChunk: true,
splitChunks: {
chunks: 'all' // 預設 entry 的 chunk 不會被拆分, 配置成 all, 就可以了
}
}
//因為是單入口檔案配置,所以沒有考慮多入口的情況,多入口是應該分別進行處理。
複製程式碼
-
對於需要事件驅動的
webpack4
配置懶載入的,可以看這篇webpack4優化教程,寫得非常全面 -
一些原生
javaScript
的DOM
操作等優化會在下面總結
六、TCP
的四次揮手,斷開連線
終結篇:效能只是 load 時間或者 DOMContentLoaded 時間的問題嗎?
RAIL
Responce
響應,研究表明,100ms內對使用者的輸入操作進行響應,通常會被人類認為是立即響應。時間再長,操作與反應之間的連線就會中斷,人們就會覺得它的操作有延遲。例如:當使用者點選一個按鈕,如果100ms內給出響應,那麼使用者就會覺得響應很及時,不會察覺到絲毫延遲感。Animaton
現如今大多數裝置的螢幕重新整理頻率是60Hz,也就是每秒鐘螢幕重新整理60次;因此網頁動畫的執行速度只要達到60FPS,我們就會覺得動畫很流暢。Idle
RAIL規定,空閒週期內執行的任務不得超過50ms,當然不止RAIL規定,W3C效能工作組的Longtasks標準也規定了超過50毫秒的任務屬於長任務,那麼50ms這個數字是怎麼得來的呢?瀏覽器是單執行緒的,這意味著同一時間主執行緒只能處理一個任務,如果一個任務執行時間過長,瀏覽器則無法執行其他任務,使用者會感覺到瀏覽器被卡死了,因為他的輸入得不到任何響應。為了達到100ms內給出響應,將空閒週期執行的任務限制為50ms意味著,即使使用者的輸入行為發生在空閒任務剛開始執行,瀏覽器仍有剩餘的50ms時間用來響應使用者輸入,而不會產生使用者可察覺的延遲。Load
如果不能在1秒鐘內載入網頁並讓使用者看到內容,使用者的注意力就會分散。使用者會覺得他要做的事情被打斷,如果10秒鐘還打不開網頁,使用者會感到失望,會放棄他們想做的事,以後他們或許都不會再回來。
如何使網頁更絲滑?
-
使用requestAnimationFrame
- 即便你能保證每一幀的總耗時都小於16ms,也無法保證一定不會出現丟幀的情況,這取決於觸發JS執行的方式。假設使用 setTimeout 或 setInterval 來觸發JS執行並修改樣式從而導致視覺變化;那麼會有這樣一種情況,因為setTimeout 或 setInterval沒有辦法保證回撥函式什麼時候執行,它可能在每一幀的中間執行,也可能在每一幀的最後執行。所以會導致即便我們能保障每一幀的總耗時小於16ms,但是執行的時機如果在每一幀的中間或最後,最後的結果依然是沒有辦法每隔16ms讓螢幕產生一次變化,也就是說,即便我們能保證每一幀總體時間小於16ms,但如果使用定時器觸發動畫,那麼由於定時器的觸發時機不確定,所以還是會導致動畫丟幀。現在整個Web只有一個API可以解決這個問題,那就是requestAnimationFrame,它可以保證回撥函式穩定的在每一幀最開始觸發。
-
避免
FSL
-
先執行
JS
,然後在JS
中修改了樣式從而導致樣式計算,然後樣式的改動觸發了佈局、繪製、合成。但JavaScript
可以強制瀏覽器將佈局提前執行,這就叫 強制同步佈局FSL
。//讀取offsetWidth的值會導致重繪 const newWidth = container.offsetWidth; //設定width的值會導致重排,但是for迴圈內部 程式碼執行速度極快,當上面的查詢操作導致的重繪 還沒有完成,下面的程式碼又會導致重排,而且這個重 排會強制結束上面的重繪,直接重排,這樣對效能影響 非常大。所以我們一般會在迴圈外部定義一個變數,這裡 面使用變數代替container.offsetWidth; boxes[i].style.width = newWidth + 'px'; } 複製程式碼
-
使用
transform
屬性去操作動畫,這個屬性是由合成器單獨處理的,所以使用這個屬性可以避免佈局與繪製。 -
使用
translateZ(0)
開啟圖層,減少重繪重排。特別在移動端,儘量使用transform
代替absolute
。建立圖層的最佳方式是使用will-change,但某些不支援這個屬性的瀏覽器可以使用3D 變形(transform: translateZ(0))來強制建立一個新層。 -
有興趣的可以看看這篇文字 前端頁面優化
-
樣式的切換最好提前定義好
class
,通過class
的切換批量修改樣式,避免多次重繪重排 -
可以先切換
display:none
再修改樣式 -
多次的
append
操作可以先插入到一個新生成的元素中,再一次性插入到頁面中。 -
程式碼複用,函式柯里化,封裝高階函式,將多次複用程式碼封裝成普通函式(俗稱方法),
React
中封裝成高階元件,ES6
中可以使用繼承,TypeScript
中介面繼承,類繼承,介面合併,類合併。 -
強力推薦閱讀:阮一峰ES6教程
以上都是根據本人的知識點總結得出,後期還會有
React
的效能優化方案等出來,路過點個贊收藏收藏~,歡迎提出問題補充~