你必須懂的前端性能優化

從輸入URL加載起看方向

從輸入 URL 到頁面加載完成的過程:javascript

  1. 首先作 DNS 查詢,若是這一步作了智能 DNS 解析的話,會提供訪問速度最快的 IP 地址回來
  2. 接下來是 TCP 握手,應用層會下發數據給傳輸層,這裏 TCP 協議會指明兩端的端口號,而後下發給網絡層。網絡層中的 IP 協議會肯定 IP 地址,而且指示了數據傳輸中如何跳轉路由器。而後包會再被封裝到數據鏈路層的數據幀結構中,最後就是物理層面的傳輸了
  3. TCP 握手結束後會進行 TLS 握手,而後就開始正式的傳輸數據
  4. 數據在進入服務端以前,可能還會先通過負責負載均衡的服務器,它的做用就是將請求合理的分發到多臺服務器上,這時假設服務端會響應一個 HTML 文件
  5. 首先瀏覽器會判斷狀態碼是什麼,若是是 200 那就繼續解析,若是 400 或 500 的話就會報錯,若是 300 的話會進行重定向,這裏會有個重定向計數器,避免過屢次的重定向,超過次數也會報錯
  6. 瀏覽器開始解析文件,若是是 gzip 格式的話會先解壓一下,而後經過文件的編碼格式知道該如何去解碼文件
  7. 文件解碼成功後會正式開始渲染流程,先會根據 HTML 構建 DOM 樹,有 CSS 的話會去構建 CSSOM 樹。若是遇到 script 標籤的話,會判斷是否存在 async 或者 defer ,前者會並行進行下載並執行 JS,後者會先下載文件,而後等待 HTML 解析完成後順序執行,若是以上都沒有,就會阻塞住渲染流程直到 JS 執行完畢。遇到文件下載的會去下載文件,這裏若是使用 HTTP 2.0 協議的話會極大的提升多圖的下載效率。
  8. 初始的 HTML 被徹底加載和解析後會觸發 DOMContentLoaded 事件
  9. CSSOM 樹和 DOM 樹構建完成後會開始生成 Render 樹,這一步就是肯定頁面元素的佈局、樣式等等諸多方面的東西
  10. 在生成 Render 樹的過程當中,瀏覽器就開始調用 GPU 繪製,合成圖層,將內容顯示在屏幕上了

咱們從輸入 URL 到顯示頁面這個過程當中,涉及到網絡層面的,有三個主要過程:html

  • DNS 解析
  • TCP 鏈接
  • HTTP 請求/響應

對於 DNS 解析和 TCP 鏈接兩個步驟,咱們前端能夠作的努力很是有限。相比之下,HTTP 鏈接這一層面的優化纔是咱們網絡優化的核心。前端

HTTP 優化有兩個大的方向:java

  • 減小請求次數
  • 減小單次請求所花費的時間

瀏覽器緩存策略

瀏覽器緩存機制有四個方面,它們按照獲取資源時請求的優先級依次排列以下:算法

  1. Memory Cache
  2. Service Worker Cache
  3. HTTP Cache
  4. Push Cache

MemoryCache

MemoryCache,是指存在內存中的緩存。從優先級上來講,它是瀏覽器最早嘗試去命中的一種緩存。從效率上來講,它是響應速度最快的一種緩存。瀏覽器秉承的是「節約原則」,咱們發現,Base64 格式的圖片,幾乎永遠能夠被塞進 memory cache,這能夠視做瀏覽器爲節省渲染開銷的「自保行爲」;此外,體積不大的 JS、CSS 文件,也有較大地被寫入內存的概率——相比之下,較大的 JS、CSS 文件就沒有這個待遇了,內存資源是有限的,它們每每被直接甩進磁盤。後端

Service Worker Cache

Service Worker 是一種獨立於主線程以外的 Javascript 線程。它脫離於瀏覽器窗體,所以沒法直接訪問 DOM。這樣獨立的個性使得 Service Worker 的「我的行爲」沒法干擾頁面的性能,這個「幕後工做者」能夠幫咱們實現離線緩存、消息推送和網絡代理等功能。咱們藉助 Service worker 實現的離線緩存就稱爲 Service Worker Cache。瀏覽器

HTTP Cache

它又分爲強緩存和協商緩存。優先級較高的是強緩存,在命中強緩存失敗的狀況下,纔會走協商緩存。緩存

對一條http get 報文的基本緩存處理過程包括7個步驟:服務器

  1. 接收
  2. 解析
  3. 查詢,緩存查看是否有本地副本可用,若是沒有,就獲取一份副本
  4. 新鮮度檢測, 緩存查看已緩存副本是否足夠新鮮,若是不是,就詢問服務器是否有任何更新。
  5. 建立響應,緩存會用新的首部和已緩存的主體來構建一條響應報文。
  6. 發送,緩存經過網絡將響應發回給客服端。
  7. 日誌

圖片描述網絡

強緩存

強緩存是利用 http 頭中的 Expires 和 Cache-Control 兩個字段來控制的。強緩存中,當請求再次發出時,瀏覽器會根據其中的 expires 和 cache-control 判斷目標資源是否「命中」強緩存,若命中則直接從緩存中獲取資源,不會再與服務端發生通訊。

是否足夠新鮮時期:
經過 Expires: XXXX XXX XXX GMT (絕對日期時間,http/1.0) 或者 Cache-Control:max-age=XXXX (相對日期時間,http/1.1)在文檔標明過時日期。

Cache-Control 相對於 expires 更加準確,它的優先級也更高。當 Cache-Control 與 expires 同時出現時,咱們以 Cache-Control 爲準。

關鍵字理解

public 與 private 是針對資源是否可以被代理服務緩存而存在的一組對立概念。若是咱們爲資源設置了 public,那麼它既能夠被瀏覽器緩存,也能夠被代理服務器緩存;若是咱們設置了 private,則該資源只能被瀏覽器緩存。private 爲默認值。

no-store與no-cache,no-cache 繞開了瀏覽器:咱們爲資源設置了 no-cache 後,每一次發起請求都不會再去詢問瀏覽器的緩存狀況,而是直接向服務端去確認該資源是否過時(即走咱們下文即將講解的協商緩存的路線)。no-store 比較絕情,顧名思義就是不使用任何緩存策略。在 no-cache 的基礎上,它連服務端的緩存確認也繞開了,只容許你直接向服務端發送請求、並下載完整的響應。

協商緩存

協商緩存依賴於服務端與瀏覽器之間的通訊。協商緩存機制下,瀏覽器須要向服務器去詢問緩存的相關信息,進而判斷是從新發起請求、下載完整的響應,仍是從本地獲取緩存的資源。若是服務端提示緩存資源未改動(Not Modified),資源會被重定向到瀏覽器緩存,這種狀況下網絡請求對應的狀態碼是 304。

協商緩存的實現:從 Last-Modified 到 Etag,詳細本身百度,這裏再也不詳細展開。

HTTP 緩存決策

圖片描述

當咱們的資源內容不可複用時,直接爲 Cache-Control 設置 no-store,拒絕一切形式的緩存;不然考慮是否每次都須要向服務器進行緩存有效確認,若是須要,那麼設 Cache-Control 的值爲 no-cache;不然考慮該資源是否能夠被代理服務器緩存,根據其結果決定是設置爲 private 仍是 public;而後考慮該資源的過時時間,設置對應的 max-age 和 s-maxage 值;最後,配置協商緩存須要用到的 Etag、Last-Modified 等參數。

Push Cache

Push Cache 是指 HTTP2 在 server push 階段存在的緩存。

  • Push Cache 是緩存的最後一道防線。瀏覽器只有在 Memory Cache、HTTP Cache 和 Service Worker Cache 均未命中的狀況下才會去詢問 Push Cache。
  • Push Cache 是一種存在於會話階段的緩存,當 session 終止時,緩存也隨之釋放。
  • 不一樣的頁面只要共享了同一個 HTTP2 鏈接,那麼它們就能夠共享同一個 Push Cache。

CDN瞭解一番

CDN 的核心點有兩個,一個是緩存,一個是回源。

「緩存」就是說咱們把資源 copy 一份到 CDN 服務器上這個過程,「回源」就是說 CDN 發現本身沒有這個資源(通常是緩存的數據過時了),轉頭向根服務器(或者它的上層服務器)去要這個資源的過程。
CDN 每每被用來存放靜態資源。所謂「靜態資源」,就是像 JS、CSS、圖片等不須要業務服務器進行計算即得的資源。而「動態資源」,顧名思義是須要後端實時動態生成的資源,較爲常見的就是 JSP、ASP 或者依賴服務端渲染獲得的 HTML 頁面。

那「非純靜態資源」呢?它是指須要服務器在頁面以外做額外計算的 HTML 頁面。具體來講,當我打開某一網站以前,該網站須要經過權限認證等一系列手段確認個人身份、進而決定是否要把 HTML 頁面呈現給我。這種狀況下 HTML 確實是靜態的,但它和業務服務器的操做耦合,咱們把它丟到CDN 上顯然是不合適的。

另外,CDN的域名必須和主業務服務器的域名不同,要不,同一個域名下面的Cookie各處跑,浪費了性能流量的開銷,CDN域名放在不一樣的域名下,能夠完美地避免了沒必要要的 Cookie 的出現!

圖片優化

二進制位數與色彩的關係

在計算機中,像素用二進制數來表示。不一樣的圖片格式中像素與二進制位數之間的對應關係是不一樣的。一個像素對應的二進制位數越多,它能夠表示的顏色種類就越多,成像效果也就越細膩,文件體積相應也會越大。

一個二進制位表示兩種顏色(0|1 對應黑|白),若是一種圖片格式對應的二進制位數有 n 個,那麼它就能夠呈現 2^n 種顏色。

計算圖片大小

對於一張 100 100 像素的圖片來講,圖像上有 10000 個像素點,若是每一個像素的值是 RGBA 存儲的話,那麼也就是說每一個像素有 4 個通道,每一個通道 1 個字節(8 位 = 1個字節),因此該圖片大小大概爲 39KB(10000 1 * 4 / 1024)。
可是在實際項目中,一張圖片可能並不須要使用那麼多顏色去顯示,咱們能夠經過減小每一個像素的調色板來相應縮小圖片的大小。
瞭解瞭如何計算圖片大小的知識,那麼對於如何優化圖片,想必你們已經有 2 個思路了:

  • 減小像素點
  • 減小每一個像素點可以顯示的顏色

圖片類型要點

JPEG/JPG 特色:有損壓縮、體積小、加載快、不支持透明,JPG 最大的特色是有損壓縮。這種高效的壓縮算法使它成爲了一種很是輕巧的圖片格式。另外一方面,即便被稱爲「有損」壓縮,JPG的壓縮方式仍然是一種高質量的壓縮方式:當咱們把圖片體積壓縮至原有體積的 50% 如下時,JPG 仍然能夠保持住 60% 的品質。但當它處理矢量圖形和 Logo 等線條感較強、顏色對比強烈的圖像時,人爲壓縮致使的圖片模糊會至關明顯。

PNG 特色:無損壓縮、質量高、體積大、支持透明,PNG(可移植網絡圖形格式)是一種無損壓縮的高保真的圖片格式。8 和 24,這裏都是二進制數的位數。按照咱們前置知識裏提到的對應關係,8 位的 PNG 最多支持 256 種顏色,而 24 位的能夠呈現約 1600 萬種顏色。PNG 圖片具備比 JPG 更強的色彩表現力,對線條的處理更加細膩,對透明度有良好的支持。它彌補了上文咱們提到的 JPG 的侷限性,惟一的 BUG 就是體積太大。

SVG 特色:文本文件、體積小、不失真、兼容性好,SVG(可縮放矢量圖形)是一種基於 XML 語法的圖像格式。它和本文說起的其它圖片種類有着本質的不一樣:SVG 對圖像的處理不是基於像素點,而是是基於對圖像的形狀描述。

Base64 特色:文本文件、依賴編碼、小圖標解決方案,Base64 並不是一種圖片格式,而是一種編碼方式。Base64 和雪碧圖同樣,是做爲小圖標解決方案而存在的。

WebP 特色:年輕的全能型選手,WebP 像 JPEG 同樣對細節豐富的圖片信手拈來,像 PNG 同樣支持透明,像 GIF 同樣能夠顯示動態圖片——它集多種圖片文件格式的優勢於一身。可是畢竟年輕,兼容性存在一些問題。

渲染優化

客戶端渲染

在客戶端渲染模式下,服務端會把渲染須要的靜態文件發送給客戶端,客戶端加載過來以後,本身在瀏覽器裏跑一遍 JS,根據 JS 的運行結果,生成相應的 DOM。頁面上呈現的內容,你在 html 源文件裏裏找不到——這正是它的特色。

服務端渲染

在服務端渲染的模式下,當用戶第一次請求頁面時,由服務器把須要的組件或頁面渲染成HTML字符串,而後把它返回給客戶端。頁面上呈現的內容,咱們在 html 源文件裏也能找到。服務端渲染解決了一個很是關鍵的性能問題——首屏加載速度過慢,也解決了SEO搜索引擎的問題。

瀏覽器渲染過程解析

瀏覽器的渲染機制通常分爲如下幾個步驟:

  1. 處理 HTML 並構建 DOM 樹。
  2. 處理 CSS 構建 CSSOM 樹
  3. 將 DOM 與 CSSOM 合併成一個渲染樹。
  4. 根據渲染樹來佈局,計算每一個節點的位置。
  5. 調用 GPU 繪製,合成圖層,顯示在屏幕上。

在渲染DOM的時候,瀏覽器所作的工做其實是:

  1. 獲取DOM後分割爲多個圖層
  2. 對每一個圖層的節點計算樣式結果(Recalculate style–樣式重計算)
  3. 爲每一個節點生成圖形和位置(Layout–迴流和重佈局)
  4. 將每一個節點繪製填充到圖層位圖中(Paint Setup和Paint–重繪)
  5. 圖層做爲紋理上傳至GPU
  6. 複合多個圖層到頁面上生成最終屏幕圖像(Composite Layers–圖層重組)

基於渲染流程的 CSS 優化建議

CSS 選擇符是從右到左進行匹配的,好比 #myList li {}實際開銷至關高。

  • 避免使用通配符,只對須要用到的元素進行選擇。
  • 關注能夠經過繼承實現的屬性,避免重複匹配重複定義。
  • 少用標籤選擇器。若是能夠,用類選擇器替代。 錯誤:#dataList li{} 正確:.dataList{}
  • 不要多此一舉,id 和 class 選擇器不該該被多餘的標籤選擇器拖後腿。錯誤:.dataList#title 正確: #title
  • 減小嵌套。後代選擇器的開銷是最高的,所以咱們應該儘可能將選擇器的深度降到最低(最高不要超過三層),儘量使用類來關聯每個標籤元素。

CSS 的阻塞

CSS 是阻塞的資源。瀏覽器在構建 CSSOM 的過程當中,不會渲染任何已處理的內容。即使 DOM 已經解析完畢了,只要 CSSOM 不 OK,那麼渲染這個事情就不 OK。咱們將 CSS 放在 head 標籤裏 和儘快 啓用 CDN 實現靜態資源加載速度的優化。

JS 的阻塞

JS 引擎是獨立於渲染引擎存在的。咱們的 JS 代碼在文檔的何處插入,就在何處執行。當 HTML 解析器遇到一個 script 標籤時,它會暫停渲染過程,將控制權交給 JS 引擎。JS 引擎對內聯的 JS 代碼會直接執行,對外部 JS 文件還要先獲取到腳本、再進行執行。等 JS 引擎運行完畢,瀏覽器又會把控制權還給渲染引擎,繼續 CSSOM 和 DOM 的構建。

DOM渲染優化

先了解迴流和重繪

  • 迴流:當咱們對 DOM 的修改引起了 DOM 幾何尺寸的變化(好比修改元素的寬、高或隱藏元素等)時,瀏覽器須要從新計算元素的幾何屬性(其餘元素的幾何屬性和位置也會所以受到影響),而後再將計算的結果繪製出來。這個過程就是迴流(也叫重排)。
  • 重繪:當咱們對 DOM 的修改致使了樣式的變化、卻並未影響其幾何屬性(好比修改了顏色或背景色)時,瀏覽器不需從新計算元素的幾何屬性、直接爲該元素繪製新的樣式(跳過了上圖所示的迴流環節)。這個過程叫作重繪。

重繪不必定致使迴流,迴流必定會致使重繪。迴流比重繪作的事情更多,帶來的開銷也更大。在開發中,要從代碼層面出發,儘量把迴流和重繪的次數最小化。

例子剖析

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>DOM操做測試</title>
</head>
<body>
  <div id="container"></div>
</body>
</html>
for(var count=0;count<10000;count++){ 
  document.getElementById('container').innerHTML+='<span>我是一個小測試</span>'  //咱們每一次循環都調用 DOM 接口從新獲取了一次 container 元素,額外開銷
}

進化一:

// 只獲取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){ 
  container.innerHTML += '<span>我是一個小測試</span>'
}

進化二:

//減小沒必要要的DOM更改
let container = document.getElementById('container')
let content = ''
for(let count=0;count<10000;count++){ 
  // 先對內容進行操做
  content += '<span>我是一個小測試</span>'
} 
// 內容處理好了,最後再觸發DOM的更改
container.innerHTML = content

事實上,考慮JS 的運行速度,比 DOM 快得多這個特性。咱們減小 DOM 操做的核心思路,就是讓 JS 去給 DOM 分壓。

在 DOM Fragment 中,DocumentFragment 接口表示一個沒有父級文件的最小文檔對象。它被當作一個輕量版的 Document 使用,用於存儲已排好版的或還沒有打理好格式的XML片斷。由於 DocumentFragment 不是真實 DOM 樹的一部分,它的變化不會引發 DOM 樹的從新渲染的操做(reflow),且不會致使性能等問題。
進化三:

let container = document.getElementById('container')
// 建立一個DOM Fragment對象做爲容器
let content = document.createDocumentFragment()
for(let count=0;count<10000;count++){
  // span此時能夠經過DOM API去建立
  let oSpan = document.createElement("span")
  oSpan.innerHTML = '我是一個小測試'
  // 像操做真實DOM同樣操做DOM Fragment對象
  content.appendChild(oSpan)
}
// 內容處理好了,最後再觸發真實DOM的更改
container.appendChild(content)

進化四:
當涉及到過萬調數據進行渲染,並且要求不卡住畫面,如何解決?
如何在不卡住頁面的狀況下渲染數據,也就是說不能一次性將幾萬條都渲染出來,而應該一次渲染部分 DOM,那麼就能夠經過 requestAnimationFrame 來每 16 ms 刷新一次。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
  </head>
  <body>
    <ul>
      控件
    </ul>
    <script>
      setTimeout(() => {
        // 插入十萬條數據
        const total = 100000
        // 一次插入 20 條,若是以爲性能很差就減小
        const once = 20
        // 渲染數據總共須要幾回
        const loopCount = total / once
        let countOfRender = 0
        let ul = document.querySelector('ul')
        function add() {
          // 優化性能,插入不會形成迴流
          const fragment = document.createDocumentFragment()
          for (let i = 0; i < once; i++) {
            const li = document.createElement('li')
            li.innerText = Math.floor(Math.random() * total)
            fragment.appendChild(li)
          }
          ul.appendChild(fragment)
          countOfRender += 1
          loop()
        }
        function loop() {
          if (countOfRender < loopCount) {
            window.requestAnimationFrame(add)
          }
        }
        loop()
      }, 0)
    </script>
  </body>
</html>

window.requestAnimationFrame() 方法告訴瀏覽器您但願執行動畫並請求瀏覽器在下一次重繪以前調用指定的函數來更新動畫。該方法使用一個回調函數做爲參數,這個回調函數會在瀏覽器重繪以前調用。
注意:若您想要在下次重繪時產生另外一個動畫畫面,您的回調例程必須調用 requestAnimationFrame()。

Event Loop

咱們先了解javascript運行機制,對渲染是大有幫助的,能夠看我歷史文章JavaScript運行機制,
Javascript運行機制深刻

事件循環中的異步隊列有兩種:macro(宏任務)隊列和 micro(微任務)隊列。
常見的 macro-task 好比: setTimeout、setInterval、 setImmediate、script(總體代碼)、 I/O 操做、UI 渲染等。
常見的 micro-task 好比: process.nextTick、Promise、MutationObserver 等。

例子分析:

// task是一個用於修改DOM的回調
setTimeout(task, 0)

上面代碼,如今 task 被推入的 macro 隊列。但由於 script 腳本自己是一個 macro 任務,因此本次執行完 script 腳本以後,下一個步驟就要去處理 micro 隊列了,再往下就去執行了一次 render,必須等待下一次的loop。

Promise.resolve().then(task)

上面代碼,咱們結束了對 script 腳本的執行,是否是緊接着就去處理 micro-task 隊列了?micro-task 處理完,DOM 修改好了,緊接着就能夠走 render 流程了——不須要再消耗多餘的一次渲染,不須要再等待一輪事件循環,直接爲用戶呈現最即時的更新結果。

當咱們須要在異步任務中實現 DOM 修改時,把它包裝成 micro 任務是相對明智的選擇。

上面說了重繪與迴流,Event loop,但不少人不知道的是,重繪和迴流其實和 Event loop 有關。

  1. 當 Event loop 執行完 Microtasks 後,會判斷 document 是否須要更新。由於瀏覽器是 60Hz 的刷新率,每 16ms 纔會更新一次。
  2. 而後判斷是否有 resize 或者 scroll ,有的話會去觸發事件,因此 resize 和 scroll 事件也是至少 16ms 纔會觸發一次,而且自帶節流功能。
  3. 判斷是否觸發了 media query
  4. 更新動畫而且發送事件
  5. 判斷是否有全屏操做事件
  6. 執行 requestAnimationFrame 回調
  7. 執行 IntersectionObserver 回調,該方法用於判斷元素是否可見,能夠用於懶加載上,可是兼容性很差
  8. 更新界面
  9. 以上就是一幀中可能會作的事情。若是在一幀中有空閒時間,就會去執行 requestIdleCallback 回調。

節流與防抖

當用戶進行滾動,觸發scroll事件,用戶的每一次滾動都將觸發咱們的監聽函數。函數執行是吃性能的,頻繁地響應某個事件將形成大量沒必要要的頁面計算。所以,咱們須要針對那些有可能被頻繁觸發的事件做進一步地優化。節流與防抖就頗有必要了!

詳細看歷史文章防抖動與節流

相關文章
相關標籤/搜索