1. 知識體系
1.1從輸入 URL 到頁面加載完成,發生了什么?
首先我們需要通過 DNS(域名解析系統)將 URL 解析為對應的 IP 地址,然后與這個 IP 地址確定的那台服務器建立起 TCP 網絡連接,隨后我們向服務端拋出我們的 HTTP 請求,服務端處理完我們的請求之后,把目標數據放在 HTTP 響應里返回給客戶端,拿到響應數據的瀏覽器就可以開始走一個渲染的流程。渲染完畢,頁面便呈現給了用戶
將這個過程切分為如下的過程片段
- DNS 解析
- TCP 連接
- HTTP 請求拋出
- 服務端處理請求,HTTP 響應返回
- 瀏覽器拿到響應數據,解析響應內容,把解析的結果展示給用戶
1.2性能優化思維導圖
2.網絡篇(http)
2.1 前端能做的網絡優化
輸入 URL 到顯示頁面這個過程中,涉及到網絡層面的,有三個主要過程:
- DNS 解析
- TCP 連接
- HTTP 請求/響應
對於 DNS 解析和 TCP 連接兩個步驟,我們前端可以做的努力非常有限。相比之下,HTTP 連接這一層面的優化才是我們網絡優化的核心
HTTP 優化有兩個大的方向
- 減少請求次數
- 減少單次請求所花費的時間
2.1 減少請求次數
2.1.1 圖片:雪碧圖,圖標字體文件
雪碧圖
多張小圖片合並為一張圖,利用CSS -background-position調整圖片顯示位置
圖標字體文件
2.1.2 合並JS和CSS文件
webpack,需要斟酌而定
2.1.3 瀏覽器緩存
如果圖片或者腳本,樣式文件內容比較固定,不經常被修改,那么,盡可能利用緩存技術,減少HTTP請求次數或文件下載次數
2.2 減少單次請求所花費的時間
主要是減少請求中數據的大小,從而達到減少單次請求所花費的時間
2.2.1 圖片
gzip
如果是vue項目,還有nginx,哪么vue,nginx,服務器都要開啟gzip
3.網絡篇(圖片優化)
3.1不同業務場景下的圖片方案選型
3.1.1前置知識:二進制位數與色彩的關系
在計算機中,像素用二進制數來表示。不同的圖片格式中像素與二進制位數之間的對應關系是不同的。一個像素對應的二進制位數越多,它可以表示的顏色種類就越多,成像效果也就越細膩,文件體積相應也會越大。
3.2 JPEG/JPG
關鍵字:有損壓縮、體積小、加載快、不支持透明
3.2.1 JPG 的優點
JPG 最大的特點是有損壓縮。這種高效的壓縮算法使它成為了一種非常輕巧的圖片格式。另一方面,即使被稱為“有損”壓縮,JPG的壓縮方式仍然是一種高質量的壓縮方式:當我們把圖片體積壓縮至原有體積的 50% 以下時,JPG 仍然可以保持住 60% 的品質。此外,JPG 格式以 24 位存儲單個圖,可以呈現多達 1600 萬種顏色,足以應對大多數場景下對色彩的要求,這一點決定了它壓縮前后的質量損耗並不容易被我們人類的肉眼所察覺——前提是你用對了業務場景。
3.2.2 使用場景
JPG 適用於呈現色彩豐富的圖片,在我們日常開發中,JPG 圖片經常作為大的背景圖、輪播圖或 Banner 圖出現。
兩大電商網站對大圖的處理,是 JPG 圖片應用場景的最佳寫照:
打開淘寶首頁,我們可以發現頁面中最醒目、最龐大的圖片,一定是以 .jpg 為后綴的:
使用 JPG 呈現大圖,既可以保住圖片的質量,又不會帶來令人頭疼的圖片體積,是當下比較推崇的一種方案。
3.2.3 JPG 的缺陷
有損壓縮在上文所展示的輪播圖上確實很難露出馬腳,但當它處理矢量圖形和 Logo 等線條感較強、顏色對比強烈的圖像時,人為壓縮導致的圖片模糊會相當明顯。
此外,JPEG 圖像不支持透明度處理,透明圖片需要召喚 PNG 來呈現。
3.3 png
關鍵字:無損壓縮、質量高、體積大、支持透明
3.3.1 PNG 的優點
PNG(可移植網絡圖形格式)是一種無損壓縮的高保真的圖片格式。8 和 24,這里都是二進制數的位數。按照我們前置知識里提到的對應關系,8 位的 PNG 最多支持 256 種顏色,而 24 位的可以呈現約 1600 萬種顏色。
PNG 圖片具有比 JPG 更強的色彩表現力,對線條的處理更加細膩,對透明度有良好的支持。它彌補了上文我們提到的 JPG 的局限性,唯一的 BUG 就是體積太大。
3.3.2PNG-8 與 PNG-24 的選擇題
什么時候用 PNG-8,什么時候用 PNG-24,這是一個問題
理論上來說,當你追求最佳的顯示效果、並且不在意文件體積大小時,是推薦使用 PNG-24 的。
但實踐當中,為了規避體積的問題,我們一般不用PNG去處理較復雜的圖像。當我們遇到適合 PNG 的場景時,也會優先選擇更為小巧的 PNG-8。
如何確定一張圖片是該用 PNG-8 還是 PNG-24 去呈現呢?好的做法是把圖片先按照這兩種格式分別輸出,看 PNG-8 輸出的結果是否會帶來肉眼可見的質量損耗,並且確認這種損耗是否在我們(尤其是你的 UI 設計師)可接受的范圍內,基於對比的結果去做判斷。
3.3.3 應用場景
前面我們提到,復雜的、色彩層次豐富的圖片,用 PNG 來處理的話,成本會比較高,我們一般會交給 JPG 去存儲。
考慮到 PNG 在處理線條和顏色對比度方面的優勢,我們主要用它來呈現小的 Logo、顏色簡單且對比強烈的圖片或背景等。
此時我們再次把目光轉向性能方面堪稱業界楷模的淘寶首頁,我們會發現它頁面上的 Logo,無論大小,還真的都是 PNG 格式:
3.4 SVG
關鍵字:文本文件、體積小、不失真、兼容性好
3.4.1 SVG 的使用方式與應用場景
- 將 SVG 寫入 HTML
- 將 SVG 寫入獨立文件后引入 HTML:
3.5 Base64
關鍵字:文本文件、依賴編碼、小圖標解決方案
3.5.1 Base64 的應用場景
- 圖片的實際尺寸很小(大家可以觀察一下掘金頁面的 Base64 圖,幾乎沒有超過 2kb 的)
- 圖片無法以雪碧圖的形式與其它小圖結合(合成雪碧圖仍是主要的減少 HTTP 請求的途徑,Base64 是雪碧圖的補充)
- 圖片的更新頻率非常低(不需我們重復編碼和修改文件內容,維護成本較低)
3.6 WebP
關鍵字:年輕的全能型選手
是 Google 專為 Web 開發的一種旨在加快圖片加載速度的圖片格式,它支持有損壓縮和無損壓縮。
3.6.1 WebP 的優點
WebP 像 JPEG 一樣對細節豐富的圖片信手拈來,像 PNG 一樣支持透明,像 GIF 一樣可以顯示動態圖片——它集多種圖片文件格式的優點於一身。
3.6.2WebP 的局限性
兼容性
3.7 總結
不同業務場景下的圖片方案選型
4.存儲篇(瀏覽器緩存)
4.1 什么是緩存
對於一個數據請求來說,可以分為發起網絡請求、后端處理、瀏覽器響應三個步驟
瀏覽器緩存可以幫助我們在第一和第三步驟中優化性能。比如說直接使用緩存而不發起請求,或者發起了請求但后端存儲的數據和前端一致,那么就沒有必要再將數據回傳回來,這樣就減少了響應數據。
緩存思維導圖
4.2 緩存位置
4.2.1 緩存優先級
從緩存位置上來說分為四種,並且各自有優先級,當依次查找緩存且都沒有命中的時候,才會去請求網絡。
- Service Worker
- Memory Cache
- Disk Cache
- Push Cache
4.2.2 Service Worker
不了解
MDN
4.2.3 MemoryCache
MemoryCache,是指存在內存中的緩存。從優先級上來說,它是瀏覽器最先嘗試去命中的一種緩存。從效率上來說,它是響應速度最快的一種緩存。
內存緩存是快的,也是“短命”的。它和渲染進程“生死相依”,當進程結束后,也就是 tab 關閉以后,內存里的數據也將不復存在。
那么哪些文件會被放入內存呢?
事實上,這個划分規則,一直以來是沒有定論的。不過想想也可以理解,內存是有限的,很多時候需要先考慮即時呈現的內存余量,再根據具體的情況決定分配給內存和磁盤的資源量的比重——資源存放的位置具有一定的隨機性
雖然划分規則沒有定論,但根據日常開發中觀察的結果,包括我們開篇給大家展示的 Network 截圖,我們至少可以總結出這樣的規律:資源存不存內存,瀏覽器秉承的是“節約原則”。我們發現,Base64 格式的圖片,幾乎永遠可以被塞進 memory cache,這可以視作瀏覽器為節省渲染開銷的“自保行為”;此外,體積不大的 JS、CSS 文件,也有較大地被寫入內存的幾率——相比之下,較大的 JS、CSS 文件就沒有這個待遇了,內存資源是有限的,它們往往被直接甩進磁盤。
4.2.4 Disk Cache
Disk Cache 也就是存儲在硬盤中的緩存,讀取速度慢點,但是什么都能存儲到磁盤中,比之 Memory Cache 勝在容量和存儲時效性上。
在所有瀏覽器緩存中,Disk Cache 覆蓋面基本是最大的。它會根據 HTTP Herder 中的字段判斷哪些資源需要緩存,哪些資源可以不請求直接使用,哪些資源已經過期需要重新請求。並且即使在跨站點的情況下,相同地址的資源一旦被硬盤緩存下來,就不會再次去請求數據。絕大部分的緩存都來自 Disk Cache,關於 HTTP 的協議頭中的緩存字段,我們會在下文進行詳細介紹
瀏覽器會把哪些文件丟進內存中?哪些丟進硬盤中
- 對於大文件來說,大概率是不存儲在內存中的,反之優先
- 當前系統內存使用率高的話,文件優先存儲進硬盤
4.2.5 Push Cache
不了解
push Cache(推送緩存)是 HTTP/2 中的內容,當以上三種緩存都沒有命中時,它才會被使用
4.3 緩存過程分析
瀏覽器與服務器通信的方式為應答模式,即是:瀏覽器發起HTTP請求 – 服務器響應該請求,那么瀏覽器怎么確定一個資源該不該緩存,如何去緩存呢?瀏覽器第一次向服務器發起該請求后拿到請求結果后,將請求結果和緩存標識存入瀏覽器緩存,瀏覽器對於緩存的處理是根據第一次請求資源時返回的響應頭來確定的。具體過程如下圖:
上圖我們可以知道
- 瀏覽器每次發起請求,都會先在瀏覽器緩存中查找該請求的結果以及緩存標識
- 瀏覽器每次拿到返回的請求結果都會將該結果和緩存標識存入瀏覽器緩存中
4.4 http緩存
HTTP 緩存是我們日常開發中最為熟悉的一種緩存機制。它又分為強緩存和協商緩存。優先級較高的是強緩存,在命中強緩存失敗的情況下,才會走協商緩存。
4.5 強緩存
強緩存:不會向服務器發送請求,直接從緩存中讀取資源,在chrome控制台的Network選項中可以看到該請求返回200的狀態碼,並且Size顯示from disk cache或from memory cache。強緩存可以通過設置兩種 HTTP Header 實現:Expires 和 Cache-Control。
4.5.1 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 后過期,需要再次請求。
- 緩存過期時間,用來指定資源的到期時間,是服務器端的具體的時間點
- 告訴瀏覽器在過期時間前瀏覽器可以直接從瀏覽器緩存取數據,而不用再次請求
- max-age的優化級高於expires,當有max-age的時候,會無視expires
- 當在有效時間內,如果服務器端的文件已經發生改變,但是瀏覽器端無法感知
4.5.2 Cache-Control
在HTTP/1.1中,Cache-Control是最重要的規則,主要用於控制網頁緩存Cache-Control 可以在請求頭或者響應頭中設置,並且可以組合使用多種指令
- max-age
- s-maxage
- private
- public
- no-cache
- no-store
max-age
max-age=xxx (xxx is numeric)表示緩存內容將在xxx秒后失效
- 設置緩存存儲的最大周期,超過這個時間緩存被認為過期(單位秒)。與Expires相反,時間是相對於請求的時間,
- 優先級高於Expires
s-maxage
- 覆蓋max-age 或者 Expires 頭,但是僅適用於共享緩存(比如各個代理),並且私有緩存中它被忽略
- 能用於public,如CDN
- 優先級高於max-age
private
所有內容只有客戶端可以緩存
表示中間節點不允許緩存,對於Browser <-- proxy1 <-- proxy2 <-- Server,proxy 會老老實實把Server 返回的數據發送給proxy1,自己不緩存任何數據。當下次Browser再次請求時proxy會做好請求轉發而不是自作主張給自己緩存的數據
- 表明響應只能被單個用戶緩存,不能作為共享緩存(即代理服務器不能緩存它),可以緩存響應內容
- 自己的服務器
public
所有內容都將被緩存(客戶端和代理服務器都可緩存)
具體來說響應可被任何中間節點緩存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中間的proxy可以緩存資源,比如下次再請求同一資源proxy1直接把自己緩存的東西給 Browser 而不再向proxy2要。
no-store
所有內容都不會被緩存,即不使用強制緩存,也不使用協商緩存
- 緩存不應存儲有關客戶端請求或服務器響應的任何內容。
- 不會使用任何緩存策略
no-cache
客戶端緩存內容,是否使用緩存則需要經過協商緩存來驗證決定。表示不使用 Cache-Control的緩存控制方式做前置驗證,而是使用 Etag 或者Last-Modified字段來控制緩存。需要注意的是,no-cache這個名字有一點誤導。設置了no-cache之后,並不是說瀏覽器就不再緩存數據,只是瀏覽器在使用緩存數據時,需要先確認一下數據是否還跟服務器保持一致
- 釋放緩存副本之前,強制高速緩存將請求提交給原始服務器進行驗證
- 這個文件不管怎么樣,都會向服務器發起請求,去服務器哪邊詢問,這個文件有沒有在緩存策略里
強緩存思維導圖
4.6 協商緩存
4.6.1 什么是協商緩存
協商緩存就是強制緩存失效后,瀏覽器攜帶緩存標識向服務器發起請求,由服務器根據緩存標識決定是否使用緩存的過程,主要有以下兩種情況:
協商緩存生效,返回304和Not Modified
協商緩存失效,返回200和請求結果
4.6.2 Last-Modified和If-Modified-Since
ast-Modified 是一個響應首部,其中包含源頭服務器認定的資源做出修改的日期及時間。 它通常被用作一個驗證器來判斷接收到的或者存儲的資源是否彼此一致。由於精確度比 ETag 要低,所以這是一個備用機制。包含有 If-Modified-Since 或 If-Unmodified-Since 首部的條件請求會使用這個字段。
- 基於客戶端和服務端協商的緩存機制
- Last-Modified ----response header
- If-Modified-Since----request header
- 需要與cache-control共同使用
- max-age的優先級高於Last-Modified
缺點
- 某些服務端不能獲取精確的修改時間
- 文件修改時間改了,但文件內容卻沒有變
4.6.3 Etag/If-None-Match
ETagHTTP響應頭是資源的特定版本的標識符。這可以讓緩存更高效,並節省帶寬,因為如果內容沒有改變,Web服務器不需要發送完整的響應。而如果內容發生了變化,使用ETag有助於防止資源的同時更新相互覆蓋(“空中碰撞”)
- 文件內容的hash值
- etag--response header
- if-none-match -- request header
- 要與cache-control共同使用
4.6.4兩者對比
- 首先在精確度上,Etag要優於Last-Modified。
- 第二在性能上,Etag要遜於Last-Modified,畢竟Last-Modified只需要記錄時間,而Etag需要服務器通過算法來計算出一個hash值。
- 第三在優先級上,服務器校驗優先考慮Etag
4.6.5 思維導圖
4.7 參考
瀏覽器緩存機制介紹與緩存策略剖析
一文讀懂前端緩存
深入理解瀏覽器的緩存機制
5. 存儲篇(本地存儲)
5.1 Cookie
- Cookie 的本職工作並非本地存儲,而是“維持狀態”。
- Cookie 不夠大
- 同一個域名下的所有請求,都會攜帶 Cookie
5.2 Local Storage,Session Storage
5.2.1 概述
- 存儲容量大: Web Storage 根據瀏覽器的不同,存儲容量可以達到 5-10M 之間
- 僅位於瀏覽器端,不與服務端發生通信。
5.2.2 Local Storage 與 Session Storage 的區別
生命周期:Local Storage 是持久化的本地存儲,存儲在其中的數據是永遠不會過期的,使其消失的唯一辦法是手動刪除;而 Session Storage 是臨時性的本地存儲,它是會話級別的存儲,當會話結束(頁面被關閉)時,存儲內容也隨之被釋放。
作用域:Local Storage、Session Storage 和 Cookie 都遵循同源策略。但 Session Storage 特別的一點在於,即便是相同域名下的兩個頁面,只要它們不在同一個瀏覽器窗口中打開,那么它們的 Session Storage 內容便無法共享。
5.2.3 應用場景
Local Storage
- 理論上 Cookie 無法勝任的、可以用簡單的鍵值對來存取的數據存儲任務,都可以交給 Local Storage 來做。
- 存儲一些內容穩定的資源。比如圖片內容豐富的電商網站會用它來存儲 Base64 格式的圖片字符串
- 存儲一些不經常更新的 CSS、JS 等靜態資源
Session Storage
微博的 Session Storage 就主要是存儲你本次會話的瀏覽足跡
5.3 IndexedDB
- IndexedDB 是沒有存儲上限的(一般來說不會小於 250M)
- IndexedDB 可以看做是 LocalStorage 的一個升級,當數據的復雜度和規模上升到了 LocalStorage 無法解決的程度,我們毫無疑問可以請出 IndexedDB 來幫忙。
6. CDN
6.1 什么是CDN
CDN (Content Delivery Network,即內容分發網絡)指的是一組分布在各個地區的服務器。這些服務器存儲着數據的副本,因此服務器可以根據哪些服務器與用戶距離最近,來滿足數據的請求。 CDN 提供快速服務,較少受高流量影響。
6.2 為什么要用 CDN
緩存、本地存儲帶來的性能提升,是不是只能在“獲取到資源並把它們存起來”這件事情發生之后?也就是說,首次請求資源的時候,這些招數都是救不了我們的。要提升首次請求的響應能力,我們還需要借助 CDN 的能力
6.3 CDN 如何工作
假設我的根服務器在杭州,同時在圖示的五個城市里都有自己可用的機房
此時有一位北京的用戶向我請求資源。在網絡帶寬小、用戶訪問量大的情況下,杭州的這一台服務器或許不那么給力,不能給用戶非常快的響應速度。於是我靈機一動,把這批資源 copy 了一批放在北京的機房里。當用戶請求資源時,就近請求北京的服務器,北京這台服務器低頭一看,這個資源我存了,離得這么近,響應速度肯定噌噌的!那如果北京這台服務器沒有 copy 這批資源呢?它會再向杭州的根服務器去要這個資源。在這個過程中,北京這台服務器就扮演着 CDN 的角色。
6.4 CDN的核心功能
CDN 的核心點有兩個,一個是緩存,一個是回源。
緩存
“緩存”就是說我們把資源 copy 一份到 CDN 服務器上這個過程
回源
就是說 CDN 發現自己沒有這個資源(一般是緩存的數據過期了),轉頭向根服務器(或者它的上層服務器)去要這個資源的過程。
6.5 CDN 與前端性能優化
CDN 往往被用來存放靜態資源
“根服務器”本質上是業務服務器,它的核心任務在於生成動態頁面或返回非純靜態頁面,這兩種過程都是需要計算的。業務服務器仿佛一個車間,車間里運轉的機器轟鳴着為我們產出所需的資源;相比之下,CDN 服務器則像一個倉庫,它只充當資源的“棲息地”和“搬運工”。
所謂“靜態資源”,就是像 JS、CSS、圖片等不需要業務服務器進行計算即得的資源。而“動態資源”,顧名思義是需要后端實時動態生成的資源,較為常見的就是 JSP、ASP 或者依賴服務端渲染得到的 HTML 頁面。
什么是“非純靜態資源”呢?它是指需要服務器在頁面之外作額外計算的 HTML 頁面。具體來說,當我打開某一網站之前,該網站需要通過權限認證等一系列手段確認我的身份、進而決定是否要把 HTML 頁面呈現給我。這種情況下 HTML 確實是靜態的,但它和業務服務器的操作耦合,我們把它丟到CDN 上顯然是不合適的。
6.6 CDN 的實際應用
靜態資源本身具有訪問頻率高、承接流量大的特點,因此靜態資源加載速度始終是前端性能的一個非常關鍵的指標。CDN 是靜態資源提速的重要手段,在許多一線的互聯網公司,“靜態資源走 CDN”並不是一個建議,而是一個規定
https://www.taobao.com/
可以看到業務服務器確實是返回給了我們一個尚未被靜態資源加持過的簡單 HTML 頁面
隨便點開一個靜態資源,可以看到它都是從 CDN 服務器上請求來的
6.7 cdn與cookie
Cookie 是緊跟域名的。同一個域名下的所有請求,都會攜帶 Cookie。大家試想,如果我們此刻僅僅是請求一張圖片或者一個 CSS 文件,我們也要攜帶一個 Cookie 跑來跑去(關鍵是 Cookie 里存儲的信息我現在並不需要),這是一件多么勞民傷財的事情。Cookie 雖然小,請求卻可以有很多,隨着請求的疊加,這樣的不必要的 Cookie 帶來的開銷將是無法想象的
同一個域名下的請求會不分青紅皂白地攜帶 Cookie,而靜態資源往往並不需要 Cookie 攜帶什么認證信息。把靜態資源和主頁面置於不同的域名下,完美地避免了不必要的 Cookie 的出現!
看起來是一個不起眼的小細節,但帶來的效用卻是驚人的。以電商網站靜態資源的流量之龐大,如果沒把這個多余的 Cookie 拿下來,不僅用戶體驗會大打折扣,每年因性能浪費帶來的經濟開銷也將是一個非常恐怖的數字。
7.渲染篇(服務端渲染)
7.1 客戶端渲染
客戶端渲染模式下,服務端會把渲染需要的靜態文件發送給客戶端,客戶端加載過來之后,自己在瀏覽器里跑一遍 JS,根據 JS 的運行結果,生成相應的 DOM
<!doctype html> <html> <head> <title>我是客戶端渲染的頁面</title> </head> <body> <div id='root'></div> <script src='index.js'></script> </body> </html>
根節點下到底是什么內容呢?你不知道,我不知道,只有瀏覽器把 index.js 跑過一遍后才知道,這就是典型的客戶端渲染。
頁面上呈現的內容,你在 html 源文件里里找不到——這正是它的特點。
7.2 服務端渲染
服務端渲染的模式下,當用戶第一次請求頁面時,由服務器把需要的組件或頁面渲染成 HTML 字符串,然后把它返回給客戶端。客戶端拿到手的,是可以直接渲染然后呈現給用戶的 HTML 內容,不需要為了生成 DOM 內容自己再去跑一遍 JS 代碼。
使用服務端渲染的網站,可以說是“所見即所得”,頁面上呈現的內容,我們在 html 源文件里也能找到。
7.3 服務端渲染解決了什么性能問題
事實上,很多網站是出於效益(seo)的考慮才啟用服務端渲染,性能倒是在其次。
假設 A 網站頁面中有一個關鍵字叫“前端性能優化”,這個關鍵字是 JS 代碼跑過一遍后添加到 HTML 頁面中的。那么客戶端渲染模式下,我們在搜索引擎搜索這個關鍵字,是找不到 A 網站的——搜索引擎只會查找現成的內容,不會幫你跑 JS 代碼。A 網站的運營方見此情形,感到很頭大:搜索引擎搜不出來,用戶找不到我們,誰還會用我的網站呢?為了把“現成的內容”拿給搜索引擎看,A 網站不得不啟用服務端渲染。
但性能在其次,不代表性能不重要。服務端渲染解決了一個非常關鍵的性能問題——首屏加載速度過慢。在客戶端渲染模式下,我們除了加載 HTML,還要等渲染所需的這部分 JS 加載完,之后還得把這部分 JS 在瀏覽器上再跑一遍。這一切都是發生在用戶點擊了我們的鏈接之后的事情,在這個過程結束之前,用戶始終見不到我們網頁的廬山真面目,也就是說用戶一直在等!相比之下,服務端渲染模式下,服務器給到客戶端的已經是一個直接可以拿來呈現給用戶的網頁,中間環節早在服務端就幫我們做掉了,用戶豈不“美滋滋”?
7.4 服務端渲染的應用場景
服務端渲染本質上是本該瀏覽器做的事情,分擔給服務器去做。這樣當資源抵達瀏覽器時,它呈現的速度就快了
但仔細想想,在這個網民遍地的時代,幾乎有多少個用戶就有多少台瀏覽器。用戶擁有的瀏覽器總量多到數不清,那么一個公司的服務器又有多少台呢?我們把這么多台瀏覽器的渲染壓力集中起來,分散給相比之下數量並不多的服務器,服務器肯定是承受不住的
除非網頁對性能要求太高了,以至於所有的招式都用完了,性能表現還是不盡人意,這時候我們就可以考慮向老板多申請幾台服務器,把服務端渲染搞起來了~
8.渲染篇(瀏覽器渲染)
8.1 瀏覽器內核
瀏覽器內核可以分成兩部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎
渲染引擎又包括了 HTML 解釋器、CSS 解釋器、布局、網絡、存儲、圖形、音視頻、圖片解碼器等等零部件。
8.2 瀏覽器渲染過程解析
8.3 基於渲染流程的 CSS 優化建議
8.3.1 CSS 選擇符是從右到左進行匹配的
#myList li {}
瀏覽器必須遍歷頁面上每個 li 元素,並且每次都要去確認這個 li 元素的父元素 id 是不是 myList
8.3.2 具體優化
- 避免使用通配符,只對需要用到的元素進行選擇
- 關注可以通過繼承實現的屬性,避免重復匹配重復定義。
- 少用標簽選擇器。如果可以,用類選擇器替代
- 不要畫蛇添足,id 和 class 選擇器不應該被多余的標簽選擇器拖后腿
- 減少嵌套。后代選擇器的開銷是最高的,因此我們應該盡量將選擇器的深度降到最低(最高不要超過三層),盡可能使用類來關聯每一個標簽元素
8.4 告別阻塞:CSS 與 JS 的加載順序優化
HTML、CSS 和 JS,都具有阻塞渲染的特性。
HTML 阻塞,天經地義——沒有 HTML,何來 DOM?沒有 DOM,渲染和優化,都是空談。
8.4.1 CSS 的阻塞
在剛剛的過程中,我們提到 DOM 和 CSSOM 合力才能構建渲染樹。這一點會給性能造成嚴重影響:默認情況下,CSS 是阻塞的資源。瀏覽器在構建 CSSOM 的過程中,不會渲染任何已處理的內容。即便 DOM 已經解析完畢了,只要 CSSOM 不 OK,那么渲染這個事情就不 OK(這主要是為了避免沒有 CSS 的 HTML 頁面丑陋地“裸奔”在用戶眼前)。
只有當我們開始解析 HTML 后、解析到 link 標簽或者 style 標簽時,CSS 才登場,CSSOM 的構建才開始。很多時候,DOM 不得不等待 CSSOM。因此我們可以這樣總結:
CSS 是阻塞渲染的資源。需要將它盡早、盡快地下載到客戶端,以便縮短首次渲染的時間。
盡早(將 CSS 放在 head 標簽里)和盡快(啟用 CDN 實現靜態資源加載速度的優化)
8.4.2 JS 的阻塞
JS 的作用在於修改,它幫助我們修改網頁的方方面面:內容、樣式以及它如何響應用戶交互。這“方方面面”的修改,本質上都是對 DOM 和 CSSDOM 進行修改。因此 JS 的執行會阻止 CSSOM,在我們不作顯式聲明的情況下,它也會阻塞 DOM。
JS 引擎是獨立於渲染引擎存在的。我們的 JS 代碼在文檔的何處插入,就在何處執行。當 HTML 解析器遇到一個 script 標簽時,它會暫停渲染過程,將控制權交給 JS 引擎。JS 引擎對內聯的 JS 代碼會直接執行,對外部 JS 文件還要先獲取到腳本、再進行執行。等 JS 引擎運行完畢,瀏覽器又會把控制權還給渲染引擎,繼續 CSSOM 和 DOM 的構建。 因此與其說是 JS 把 CSS 和 HTML 阻塞了,不如說是 JS 引擎搶走了渲染引擎的控制權。
8.4.3 JS的三種加載方式
正常模式
這種情況下 JS 會阻塞瀏覽器,瀏覽器必須等待 index.js 加載和執行完畢才能去做其它事情。
<script src="index.js"></script>
async(異步) 模式
async 模式下,JS 不會阻塞瀏覽器做任何其它的事情。它的加載是異步的,當它加載結束,JS 腳本會立即執行。
<script async src="index.js"></script>
defer(延緩) 模式
efer 模式下,JS 的加載是異步的,執行是被推遲的。等整個文檔解析完成、DOMContentLoaded 事件即將被觸發時,被標記了 defer 的 JS 文件才會開始依次執行。
<script defer src="index.js"></script>
從應用的角度來說,一般當我們的腳本與 DOM 元素和其它腳本之間的依賴關系不強時,我們會選用 async;當腳本依賴於 DOM 元素和其它腳本的執行結果時,我們會選用 defer。
9. 渲染篇(dom優化)
10. 渲染篇(Event Loop與異步更新策略(vue))
10.1 什么是異步更新?
當我們使用 Vue 或 React 提供的接口去更新數據時,這個更新並不會立即生效,而是會被推入到一個隊列里。待到適當的時機,隊列中的更新任務會被批量觸發。這就是異步更新。
異步更新可以幫助我們避免過度渲染,是我們上節提到的“讓 JS 為 DOM 分壓”的典范之一。
10.2 異步更新的優越性
異步更新的特性在於它只看結果,因此渲染引擎不需要為過程買單
有時我們會遇到這樣的情況
// 任務一 this.content = '第一次測試' // 任務二 this.content = '第二次測試' // 任務三 this.content = '第三次測試'
我們在三個更新任務中對同一個狀態修改了三次,如果我們采取傳統的同步更新策略,那么就要操作三次 DOM。但本質上需要呈現給用戶的目標內容其實只是第三次的結果,也就是說只有第三次的操作是有意義的——我們白白浪費了兩次計算。
但如果我們把這三個任務塞進異步更新隊列里,它們會先在 JS 的層面上被批量執行完畢。當流程走到渲染這一步時,它僅僅需要針對有意義的計算結果操作一次 DOM——這就是異步更新的妙處。
10.3 Vue狀態更新手法:nextTick
Vue在觀察到數據變化時並不是直接更新DOM,而是開啟一個隊列,並緩沖在同一事件循環中發生的所有數據改變。在緩沖時會去除重復數據,從而避免不必要的計算和DOM操作。然后,在下一個事件循環tick中,Vue刷新隊列並執行實際工作
$nextTick就是用來知道什么時候DOM更新完成的
11. 渲染篇(回流與重繪)
12. 事件的節流與防抖
12.1 為什么會出現事件的節流與防抖
scroll 事件是一個非常容易被反復觸發的事件。其實不止 scroll 事件,resize 事件、鼠標事件(比如 mousemove、mouseover 等)、鍵盤事件(keyup、keydown 等)都存在被頻繁觸發的風險。
頻繁觸發回調導致的大量計算會引發頁面的抖動甚至卡頓。為了規避這種情況,我們需要一些手段來控制事件被觸發的頻率。就是在這樣的背景下,throttle(事件節流)和 debounce(事件防抖)出現了。
12.2 節流與防抖的本質
這兩個東西都以閉包的形式存在。
它們通過對事件對應的回調函數進行包裹、以自由變量的形式緩存時間信息,最后用 setTimeout 來控制事件的觸發頻率。
12.3 節流
在某段時間內,不管你觸發了多少次回調,我都只認第一次,並在計時結束時給予響應。
現在有一個旅客剛下了飛機,需要用車,於是打電話叫了該機場唯一的一輛機場大巴來接。司機開到機場,心想來都來了,多接幾個人一起走吧,這樣這趟才跑得值——我等個十分鍾看看。於是司機一邊打開了計時器,一邊招呼后面的客人陸陸續續上車。在這十分鍾內,后面下飛機的乘客都只能乘這一輛大巴,十分鍾過去后,不管后面還有多少沒擠上車的乘客,這班車都必須發走
12.4 防抖
在某段時間內,不管你觸發了多少次回調,我都只認最后一次。
第一個乘客上車后,司機開始計時(比如說十分鍾)。十分鍾之內,如果又上來了一個乘客,司機會把計時器清零,重新開始等另一個十分鍾(延遲了等待)。直到有這么一位乘客,從他上車開始,后續十分鍾都沒有新乘客上車,司機會認為確實沒有人需要搭這趟車了,才會把車開走。
12.5 防抖的缺憾
防抖 的問題在於它“太有耐心了”。試想,如果用戶的操作十分頻繁——他每次都不等 防抖 設置的 delay 時間結束就進行下一次操作,於是每次 防抖 都為該用戶重新生成定時器,回調函數被延遲了不計其數次。頻繁的延遲會導致用戶遲遲得不到響應,用戶同樣會產生“這個頁面卡死了”的觀感。
為了避免弄巧成拙,我們需要借力 節流 的思想,打造一個“有底線”的 防抖——等你可以,但我有我的原則:delay 時間內,我可以為你重新生成定時器;但只要delay的時間到了,我必須要給用戶一個響應。這個 節流與 防抖 “合體”思路,已經被很多成熟的前端庫應用到了它們的加強版 節流 函數的實現中
12.6 demo
<!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> <style> .box { height: 2000px; background: red; } </style> </head> <body> <div class="box">11</div> <script> function throttle(fn, delay) { // last為上一次觸發回調的時間, timer是定時器 let last = 0, timer = null // 將throttle處理結果當作函數返回 return function () { // 保留調用時的this上下文 let context = this // 保留調用時傳入的參數 let args = arguments // 記錄本次觸發回調的時間 let now = +new Date() // 判斷上次觸發的時間和本次觸發的時間差是否小於時間間隔的閾值 if (now - last < delay) { // 如果時間間隔小於我們設定的時間間隔閾值,則為本次觸發操作設立一個新的定時器 clearTimeout(timer) timer = setTimeout(function () { last = now fn.apply(context, args) }, delay) } else { // 如果時間間隔超出了我們設定的時間間隔閾值,那就不等了,無論如何要反饋給用戶一次響應 last = now fn.apply(context, args) } } } // 用新的throttle包裝scroll的回調 const better_scroll = throttle(() => console.log('觸發了滾動事件'), 1000) document.addEventListener('scroll', better_scroll) </script> </body> </html>
11 性能監控
performance
function getsec(time) { return time / 1000 + 's' } window.onload = function () { var per = window.performance.timing; console.log('DNS查詢耗時' + getsec(per.domainLookupEnd - per.domainLookupStart)) console.log('TCP鏈接耗時' + getsec(per.connectEnd - per.connectStart)) console.log('request請求響應耗時' + getsec(per.responseEnd - per.responseStart)) console.log('dom渲染耗時' + getsec(per.domComplete - per.domInteractive)) console.log('白屏時間' + getsec(firstPaint - pageStartTime)) console.log('domready可操作時間' + getsec(per.domContentLoadedEventEnd - per.fetchStart)) }