“春江水暖鴨先知,產品好壞客戶知”,作為前端開發,我們更注重客戶體驗,產品的好壞決定著客戶的體驗,那么一款產品的好壞有很多因素,其中性能是決定因素,那么怎么優化才能讓產品的性能達到優良,讓客戶體驗良好,今天我就帶大家去了解學習前端性能優化。
優化的目的
優化的目的在于讓頁面加載的更快,對用戶操作響應更及時,為用戶帶來更好的用戶體驗,對于開發者來說優化能夠減少頁面請求數,能夠節省資源。
前端優化的方法有很多種,可以將其分為兩大類,第一類是頁面級別的優化如http請求數,內聯腳本的位置優化等,第二類為代碼級別的優化,例JAVAscript中的DOM 操作優化、css選擇符優化、圖片優化以及 html結構優化等等。
優化哪些?
那么我們需要優化那些點呢?
- 加載資源優化
- 渲染優化
- 瀏覽器緩存策略
- 圖片優化
- 節流與防抖
加載資源優化
說起加載,當我們輸入URL時,我們要知道這中間發生了什么?
- 首先做 DNS 查詢,如果這一步做了智能 DNS 解析的話,會提供訪問速度最快的 IP 地址回來
- 接下來是 TCP 握手,應用層會下發數據給傳輸層,這里 TCP 協議會指明兩端的端口號,然后下發給網絡層。網絡層中的 IP 協議會確定 IP 地址,并且指示了數據傳輸中如何跳轉路由器。然后包會再被封裝到數據鏈路層的數據幀結構中,最后就是物理層面的傳輸了
- TCP 握手結束后會進行 TLS 握手,然后就開始正式的傳輸數據
- 數據在進入服務端之前,可能還會先經過負責負載均衡的服務器,它的作用就是將請求合理的分發到多臺服務器上,這時假設服務端會響應一個 HTML 文件
- 首先瀏覽器會判斷狀態碼是什么,如果是 200 那就繼續解析,如果 400 或 500 的話就會報錯,如果 300 的話會進行重定向,這里會有個重定向計數器,避免過多次的重定向,超過次數也會報錯
- 瀏覽器開始解析文件,如果是 gzip 格式的話會先解壓一下,然后通過文件的編碼格式知道該如何去解碼文件
- 文件解碼成功后會正式開始渲染流程,先會根據 HTML 構建 DOM 樹,有 CSS 的話會去構建 CSSOM 樹。如果遇到 script 標簽的話,會判斷是否存在 async 或者 defer ,前者會并行進行下載并執行 JS,后者會先下載文件,然后等待 HTML 解析完成后順序執行,如果以上都沒有,就會阻塞住渲染流程直到 JS 執行完畢。遇到文件下載的會去下載文件,這里如果使用 HTTP 2.0 協議的話會極大的提高多圖的下載效率。
- 初始的 HTML 被完全加載和解析后會觸發 DOMContentLoaded 事件
- CSSOM 樹和 DOM 樹構建完成后會開始生成 Render 樹,這一步就是確定頁面元素的布局、樣式等等諸多方面的東西
- 在生成 Render 樹的過程中,瀏覽器就開始調用 GPU 繪制,合成圖層,將內容顯示在屏幕上了
我們從輸入 URL 到顯示頁面這個過程中,涉及到網絡層面的,有三個主要過程:
- DNS 解析
- TCP 連接
- HTTP 請求/響應
這里我們就不用去管DNS解析和TCP鏈接了,畢竟不是我們的事,也干不來,但是HTTP請求和響應是我們優化的重點。
HTTP優化可分為兩個方面:
- 盡量減少請求次數
- 盡量減少單次請求所花費的時間
減少請求數:
- 合理的設置http緩存,恰當的緩存設置可以大大減少http請求。要盡可能地讓資源能夠在緩存中待得更久。
- 從設計實現層面簡化頁面,保持頁面簡潔、減少資源的使用時是最直接的。
- 資源合并與壓縮,盡可能的將外部的腳本、樣式進行合并,多個合為一個。
- CSS Sprites,通過合并 CSS圖片,這是減少請求數的一個好辦法
內聯腳本的位置:
瀏覽器是并發請求的,很多時候我們會加入很多的外鏈腳本,而外鏈腳本在加載時卻常常阻塞其他資源,例如在腳本加載完成之前,它后面的圖片、樣式以及其他腳本都處于阻塞狀態,直到腳本加載完成后才會開始加載。如果將腳本放在比較靠前的位置,則會影響整個頁面的加載速度從而影響用戶體驗。所以說盡可能的將腳本往后挪,減少對并發下載的影響。
渲染優化
客戶端的渲染
前端去取后端的數據生成DOM樹,加載過來后,自己在瀏覽器由上而下跑執行JS,隨后就會生成相應的DOM。
優點:
- 客戶端的渲染使得前后端分離,開發效率高
- 用戶體驗更好,我們將網站做成SPA(單頁面應用)或者部分內容做成SPA,當用戶點擊時,不會形成頻繁的跳轉
缺點:
- 前端響應速度慢,特別是首屏,這樣用戶是受不了的
- 不利于seo優化,因為爬蟲不認識SPA,所以它只是記錄了一個頁面
服務端的渲染
DOM樹在服務端生成,然后返回給前端,頁面上展現的內容,我們在HTML源文件也能找到。
優點:
- 服務端渲染盡量不占用前端的資源,前端這塊耗時少,速度快
- 利于SEO優化,因為在后端有完整的html頁面,所以爬蟲更容易爬取信息
缺點:
- 不利于前后端分離,開發的效率降低了
- 對html的解析,對前端來說加快了速度,但是加大了服務器的壓力
類似企業級網站,主要功能是頁面展示,它沒有復雜的交互,并且需要良好的SEO,那我們應該使用服務端渲染。
現在很多網站使用服務端渲染和客戶端渲染結合的方式:首屏使用服務端渲染,其他頁面使用客戶端渲染。這樣可以保證首屏的加載速度,也完成了前后端分離。
區分:源碼里如果能找到前端頁面中的內容文字,那就是在服務端構建的DOM,就是服務端渲染,反之是客戶端渲染。
瀏覽器渲染
瀏覽器渲染機制一般分為:
- 分析HTML并構建DOM樹
- 分析CSS構建CSSOM樹
- 將DOM和CSSOM合并成一個渲染樹
- 根據渲染樹布局,計算每個節點的位置
- 調用GPU繪制,合成圖層,顯示頁面
在渲染DOM的時候,瀏覽器所做的事情:
- 獲取DOM后分割為多個圖層
- 對每個圖層的節點計算樣式結果(recalculate style -- 樣式重計算)
- 為每個節點生成圖形和位置(layout -- 回流和重布局)
- 將每個節點繪制填充到圖層位圖中(paint setup 和 paint -- 重繪)
- 圖層作為紋理上傳至GPU
- 復合多個圖層到頁面上生成最終屏幕圖像(composite layers -- 圖層重組)
新建獨立圖層會減少重回回流帶來的影響,但是在圖層重組的時候會消耗大量的性能,所以要權衡利弊,有所選擇。
渲染流程的CSS優化
CSS的渲染是從右到左進行匹配的,我們應該注意:
- 避免大量使用通配符,可選擇需要用到的元素
- 關注可以通過繼承實現的屬性,避免重復匹配,重復定義
- 少用標簽選擇器,例如.header ul li a
- id和class選擇器不應該被多余的選擇器拖后腿,例如.header#title
- 減少嵌套,后代選擇器的開銷最高,不要一大串,要將選擇器的深度降到最低,盡可能使用類來關聯每一個標簽元素。
CSS阻塞
我們將css放在head標簽里和盡快啟用CDN實現靜態資源加載速度的優化,因為只要CSSOM不OK,那么渲染就不會完成。
JS阻塞
JS引擎是獨立于渲染引擎存在的,就是說插在頁面那,就在那執行,瀏覽器遇到script標簽時,它就會停止交于JS引擎渲染,等它渲染完,瀏覽器又交于渲染引擎繼續CSSOM和DOM的構建。
DOM渲染優化
也就是說重繪回流問題
- 回流:前面我們通過構造渲染樹,我們將可見DOM節點以及它對應的樣式結合起來,可是我們還需要計算它們在設備視口(viewport)內的確切位置和大小,這個計算的階段就是回流。
- 重繪:最終,我們通過構造渲染樹和回流階段,我們知道了哪些節點是可見的,以及可見節點的樣式和具體的幾何信息(位置、大小),那么我們就可以將渲染樹的每個節點都轉換為屏幕上的實際像素,這個階段就叫做重繪節點。
當頁面布局和幾何信息發生變化的時候,就需要回流。比如以下情況:
- 添加或刪除可見的DOM元素
- 元素的位置發生變化
- 元素的尺寸發生變化(包括外邊距、內邊框、邊框大小、高度和寬度等)
- 內容發生變化,比如文本變化或圖片被另一個不同尺寸的圖片所替代。
- 頁面一開始渲染的時候(這肯定避免不了)
- 瀏覽器的窗口尺寸變化(因為回流是根據視口的大小來計算元素的位置和大小的)
注意:回流一定會觸發重繪,而重繪不一定會回流,回流比重繪做的事情要多,帶來的開銷也大,在開發中,要從代碼層面出發,盡可能把回流和重繪的次數最小化。
如何最小化重繪和重排
- 用 translate 替代 top
- 用 opacity 替代 visibility
- 不要一條一條的修改 DOM 的樣式,預先定義好 class,然后修改 DOM 的 className
- 把 DOM 離線后修改,比如:先把 DOM 給 display: none(有一次 reflow),然后修改100次,然后再顯示出來
- 不要把 DOM 節點的屬性值放在一個循環里當成循環里的變量
- 不要使用 table 布局,可能很小的一個改動就會造成整個 table 的重新布局
- 動畫實現的速度的選擇
- 對于動畫新建圖層
- 啟用 GPU 硬件加速
瀏覽器緩存
強緩存
發現有緩存直接用。
Expires: 絕對時間,判斷客戶端日期是否超過這個時間
Cache-Control:相對時間,判斷訪問間隔是否大于3600秒
//在設定時間之前不會和服務端進行通信了
//如果兩個都下發以后者為準
協商緩存
詢問服務器緩存是否可以用,在進行判斷是否用。
Last-Modified/If-Modified-Since
第一次請求,respone的header加上Last-Modified(最后修改時間)
再次請求,在request的header上加上If-Modified-Since
和服務端的最后修改時間對比,如果沒有變化則返回304 Not Modified,但是不會返回資源內容;如果有變化,就正常返回資源內容。瀏覽器收到304的響應后,就會從緩存中加載資源
如果協商緩存沒有命中,瀏覽器直接從服務器加載資源時,Last-Modified的Header在重新加載的時候會被更新
Etag/If-None-Match
這兩個值是由服務器生成的每個資源的唯一標識字符串,只要資源有變化就這個值就會改變;其判斷過程與Last-Modified/If-Modified-Since類似,他可以精確到秒的更高級別。
DNS預解析
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.web.net">
在一些瀏覽器的a標簽是默認打開dns預解析的,在https協議下dns預解析是關閉的,加入mate后會打開。
圖片優化
減小圖片大小
我看到有的文章通過計算圖片大小來優化圖片,就是說:
比如一張100*100的圖片,圖片上有10000個像素點,如果每個像素的值是RGBA存儲的話,那么也就是說每個像素有4個通道,每個通道1個字節(8位 = 1個字節),所以該圖片的大小大概為39KB。所以說通過:
- 減少像素點
- 減少每個像素點能夠顯示的顏色
上面的兩種方式減小圖片的大小,不過在我們開發中直接壓縮來減小圖片的大小。
改變圖片的格式
這個圖片的類型也決定著圖片的屬性,詳細的我再微頭條說過,附上鏈接:
各種圖片格式的特點
節流和防抖
日常開發過程中,滾動事件做復雜計算頻繁調用回調函數很可能會造成頁面的卡頓,這時候我們更希望把多次計算合并成一次,只操作一個精確點,JS把這種方式稱為debounce(防抖)和throttle(節流)
函數節流
當持續觸發事件時,保證在一定時間內只調用一次事件處理函數,意思就是說,假設一個用戶一直觸發這個函數,且每次觸發小于既定值,函數節流會每隔這個時間調用一次
用一句話總結防抖和節流的區別:防抖是將多次執行變為最后一次執行,節流是將多次執行變為每隔一段時間執行
實現函數節流我們主要有兩種方法:時間戳和定時器
var throttle = function(func, delay) {
var prev = Date.now();
return function() {
var context = this; //this指向window
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.Apply(context, args);
prev = Date.now();
}
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
這個節流函數利用時間戳讓第一次滾動事件執行一次回調函數,此后每隔1000ms執行一次,在小于1000ms這段時間內的滾動是不執行的
函數防抖
當持續觸發事件時,一定時間段內沒有再觸發事件,事件處理函數才會執行一次,如果設定時間到來之前,又觸發了事件,就重新開始延時。也就是說當一個用戶一直觸發這個函數,且每次觸發函數的間隔小于既定時間,那么防抖的情況下只會執行一次。
function debounce(fn, wait) {
var timeout = null; //定義一個定時器
return function() {
if(timeout !== null)
clearTimeout(timeout); //清除這個定時器
timeout = setTimeout(fn, wait);
}
}
// 處理函數
function handle() {
console.log(Math.random());
}
// 滾動事件
window.addEventListener('scroll', debounce(handle, 1000));
如上所見,當持續觸發scroll函數,handle函數只會在1秒時間內執行一次,在滾動過程中并沒有持續執行,有效減少了性能的損耗。
防抖和節流能有效減少瀏覽器引擎的損耗,防止出現頁面堵塞卡頓現象。
總結
上面我們主要從加載資源優化、渲染優化、瀏覽器緩存策略、圖片優化、節流與防抖這幾個方面,講述了我們平常不易掌握和了解的性能優化知識點,希望大家可以了解學習掌握并加以應用,讓我們的產品體驗更佳,精益求精,做最好的產品和自己。