你所不知道的前端效能優化不完全手冊

Jeery_譚金傑發表於2019-04-10

效能優化是一門大學問,本文僅對個人一些積累知識的闡述,歡迎下面補充。

丟擲一個問題,從輸入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連線在傳送完最後一個響應後,還需要holdkeepalive_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優化教程,寫得非常全面

  • 一些原生javaScriptDOM操作等優化會在下面總結

六、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教程

  • 以及什麼是TypeScript以及入門

以上都是根據本人的知識點總結得出,後期還會有React的效能優化方案等出來,路過點個贊收藏收藏~,歡迎提出問題補充~

相關文章