性能優(yōu)化之關(guān)鍵渲染路徑

不要再問“那怎么可能”,而是問“為什么不能”

大家好,我是柒八九。

今天,我們來談?wù)?,瀏覽器的關(guān)鍵渲染路徑。針對瀏覽器的一些其他文章,我們前面有介紹。分別從瀏覽器架構(gòu)最新的渲染引擎介紹了關(guān)于頁面渲染的相關(guān)概念。對應(yīng)連接如下。

而今天的主角是<span style="font-weight:800;color:#FFA500;font-size:18px">{關(guān)鍵渲染路徑| Critical Rendering Path}</span>。它是影響頁面在加載階段的主要標(biāo)準(zhǔn)。

這里再啰嗦一點(diǎn),通常一個頁面有三個階段

  1. 加載階段
    • 是指從發(fā)出請求到渲染出完整頁面的過程
    • 影響到這個階段的主要因素有網(wǎng)絡(luò)JavaScript 腳本
  2. 交互階段
    • 主要是從頁面加載完成到用戶交互的整個過程
    • 影響到這個階段的主要因素是 JavaScript 腳本
  3. 關(guān)閉階段
    • 主要是用戶發(fā)出關(guān)閉指令后頁面所做的一些清理操作

好了,時間不早了。開干。

你能所學(xué)到的知識點(diǎn)

  1. 關(guān)鍵渲染路徑的各種指標(biāo)
  2. <span style="font-weight:800;color:#FFA500;font-size:18px">{關(guān)鍵資源| Critical Resource}</span>:所有可能阻礙頁面渲染的資源
  3. <span style="font-weight:800;color:#FFA500;font-size:18px">{關(guān)鍵路徑長度|Critical Path Length}</span>:獲取構(gòu)建頁面所需的所有關(guān)鍵資源所需的 RTT(Round Trip Time)
  4. <span style="font-weight:800;color:#FFA500;font-size:18px">{關(guān)鍵字節(jié)| Critical Bytes}</span>:作為完成和構(gòu)建頁面的一部分而傳輸?shù)?strong>字節(jié)總數(shù)。
  5. 重溫HTTP緩存
  6. 針對關(guān)鍵渲染路徑進(jìn)行各種優(yōu)化處理
  7. 針對React應(yīng)用做優(yōu)化處理

1. 加載階段關(guān)鍵數(shù)據(jù)

<span style="font-weight:800;color:#FFA500;font-size:18px">{文檔對象模型| Document Object Model}</span>

DOM:是HTML頁面在解析后,基于對象的表現(xiàn)形式。

DOM是一個應(yīng)用編程接口(API),通過創(chuàng)建表示文檔的樹,以一種獨(dú)立于平臺和語言的方式訪問和修改一個頁面的內(nèi)容和結(jié)構(gòu)。

HTML 文檔中,Web開發(fā)者可以使用JS來CRUD DOM 結(jié)構(gòu),其主要的目的是動態(tài)改變HTML文檔的結(jié)構(gòu)。

DOM 將整個HTML頁面抽象為一組分層節(jié)點(diǎn)

DOM 并非只能通過 JS 訪問, 像<span style="font-weight:700;color:green;">{可伸縮矢量圖| SVG}</span>、<span style="font-weight:700;color:green;">{數(shù)學(xué)標(biāo)記語言| MathML}</span>和<span style="font-weight:700;color:green;">{同步多媒體集成語言| SMIL}</span>都增加了該語言獨(dú)有的 DOM 方法和接口。

一旦HTML被解析,就會建立一個DOM樹

下面的代碼有三個區(qū)域:header、mainfooter。并且style.css外部文件。

<html>
  <head>
  <link rel="stylesheet" href="style.css">
  <title>關(guān)鍵渲染路徑示例</title>
  <body>
    <header>
      <h1>...</h1>
      <p>...</p>
    </header>
    <main>
         <h1>...</h1>
         <p>...</p>
    </main>
    <footer>
         <small>...</small>
    </footer>
  </body> 
  </head>
</html>

當(dāng)上述 HTML 代碼被瀏覽器解析為 DOM樹狀結(jié)構(gòu)時,其各個節(jié)點(diǎn)的關(guān)系如下。

DOM樹

每個瀏覽器都需要一些時間解析HTML。并且,清晰的語義標(biāo)記有助于減少瀏覽器解析HTML所需的時間。(不完整或者錯誤的語義標(biāo)記,還需要瀏覽器根據(jù)上下文去分析和判斷)

具體,瀏覽器是如何將HTML字符串信息,轉(zhuǎn)換成能夠被JS操作的DOM對象,不在此文的討論范圍內(nèi)。不過,我們可以舉一個很小的例子。在我們JS算法探險之棧(Stack)中,有一個題就是如何判斷括號的正確性。

給定一個只包括 '(',')','{','}','[',']' 的字符串 s ,判斷字符串是否有效。 有效字符串需滿足:

左括號必須用相同類型的右括號閉合。

左括號必須以正確的順序閉合。

示例:

輸入:s = "()[]{}" 輸出:true

輸入:s = "(]" 輸出:false

其實(shí),上面的例子就是最簡單的一種標(biāo)簽匹配?;蛘哒f的穩(wěn)妥點(diǎn),它們的主要思想是一致的。


CSSOM Tree

CSSOM也是一個基于對象的樹。它負(fù)責(zé)處理與DOM樹相關(guān)的樣式。

承接上文,我們這里有和上面HTML配套的CSS樣式。

header{
   background-color: white;
   color: black;
}
p{
   font-weight:400;
}
h1{
   font-size:72px;
}
small{
   text-align:left
}

對于上述CSS聲明,CSSOM樹將顯示如下。

CSSOM樹

由于,css的部分屬性能夠被繼承,所以,在父級節(jié)點(diǎn)定義的屬性,如果滿足情況,子節(jié)點(diǎn)也是會有對應(yīng)的屬性信息,最后將對應(yīng)的樣式信息,渲染到頁面上。

一般來說,CSS被認(rèn)為是一種<span style="font-weight:800;color:#FFA500;font-size:18px">{阻斷渲染| Render-Blocking}</span>資源。

什么是渲染阻斷?渲染阻塞資源是一個組件,它將不允許瀏覽器渲染整個DOM樹,直到給定的資源被完全加載。
CSS 是一種渲染阻斷資源,因?yàn)樵贑SS完全加載之前,你無法渲染樹。

起初,頁面中所有CSS信息都被存放在一個文件中 ?,F(xiàn)在,開發(fā)人員通過一些技術(shù)手段,能夠?qū)?code>CSS文件分割開來,只在渲染的早期階段提供關(guān)鍵樣式。


執(zhí)行JS

先將一個小知識點(diǎn),其實(shí),在前面的文章中,我們已經(jīng)講過了。這里,我們再啰嗦一遍。

瀏覽器環(huán)境下,JS = ECMAScript + DOM + BOM。

ECMAScript

JS的核心部分,即 ECMA-262 定義的語言,并不局限于 Web 瀏覽器。

Web 瀏覽器只是 ECMAScript 實(shí)現(xiàn)可能存在的一種<span style="font-weight:800;color:#FFA500;font-size:18px">{宿主環(huán)境| Host Environment}</span>。而宿主環(huán)境提供 ECMAScript基準(zhǔn)實(shí)現(xiàn)和與環(huán)境自身交互必需的擴(kuò)展。(比如 DOM 使用 ECMAScript 核心類型和語法,提供特定于環(huán)境的額外功能)。

像我們比較常見的Web 瀏覽器、 Node.js和已經(jīng)被淘汰的 Adobe Flash都是ECMA的宿主環(huán)境。

ECMAScript 只是對實(shí)現(xiàn)ECMA-262規(guī)范的一門語言的稱呼, JS 實(shí)現(xiàn)了 ECMAScript,Adobe ActionScript 也實(shí)現(xiàn) ECMAScript。

上面的內(nèi)容只是做一個知識點(diǎn)的補(bǔ)充,我們這篇文章中出現(xiàn)的JS還是一般意義上的含義:即javascript文本信息。


JavaScript 是一種用來操作DOM的語言。這些操作花費(fèi)時間,并增加網(wǎng)站的整體加載時間。所有,

JavaScript 代碼被稱為 <span style="font-weight:800;color:#FFA500;font-size:18px">{解析器阻塞| Parser Blocking}</span>資源。

什么是解析器阻塞?當(dāng)需要下載執(zhí)行JavaScript代碼時,瀏覽器會暫停執(zhí)行和構(gòu)建DOM樹。當(dāng)JavaScript代碼被執(zhí)行完后,DOM樹的構(gòu)建才繼續(xù)進(jìn)行。

所以才有, JavaScript是一種昂貴的資源的說法。


示例演示

下面是一段HTML代碼的演示結(jié)果,顯示了一些文字和圖片。正如你所看到的,整個頁面的顯示只花了大約40ms。即使有一張圖片,頁面顯示的時間也更短。這是因?yàn)樵谶M(jìn)行第一次繪制時,圖像沒有被當(dāng)作關(guān)鍵資源

記住,

<span style="font-weight:800;color:#FFA500;font-size:18px">{關(guān)鍵渲染路徑| Critical Rendering Path}</span>都是關(guān)于HTML、CSSJavascript

現(xiàn)在,在這段代碼中添加css。正如下圖所示,一個額外的請求被觸發(fā)了。盡管加載html文件的時間減少了,但處理和顯示頁面的總體時間卻增加了近10倍。為什么呢?

  • 普通的HTML并不涉及太多的資源獲取解析工作。但是,對于CSS文件,必須構(gòu)建一個CSSOM。HTMLDOMCSSCSSOM 都必須被構(gòu)建。這無疑是一個耗時的過程。

  • JavaScript 很有可能會查詢 CSSOM。這意味著,在執(zhí)行任何JavaScript之前,CSS文件必須被完全下載和解析。

注意domContentLoadedHTML DOM完全解析和加載時被觸發(fā)。該事件不會等待image、子frame甚至是樣式表被完全加載。唯一的目標(biāo)是文檔被加載??梢栽?code>window中添加事件,以查看DOM是否被解析和加載。

window.addEventListener('DOMContentLoaded', (event) => {
    console.log('DOM被解析且加載成功');
});

即使你選擇用內(nèi)聯(lián)腳本取代外部文件,性能也不會有大的改變。主要是因?yàn)樾枰獦?gòu)建CSSOM。如果你考慮使用外部腳本,可以添加 async屬性。這將解除對解析器的阻斷。


關(guān)鍵路徑相關(guān)術(shù)語

  • <span style="font-weight:800;color:#FFA500;font-size:18px">{關(guān)鍵資源| Critical Resource}</span>:所有可能阻礙頁面渲染的資源

  • <span style="font-weight:800;color:#FFA500;font-size:18px">{關(guān)鍵路徑長度|Critical Path Length}</span>:獲取構(gòu)建頁面所需的所有關(guān)鍵資源所需的 RTT(Round Trip Time)

    • 當(dāng)使用 TCP 協(xié)議傳輸一個文件時,由于 TCP 的特性,這個數(shù)據(jù)并不是一次傳輸?shù)椒?wù)端的,而是需要拆分成一個個數(shù)據(jù)包來回多次進(jìn)行傳輸?shù)?/li>
    • RTT 就是這里的往返時延
      • 它是網(wǎng)絡(luò)中一個重要的性能指標(biāo)表示從發(fā)送端發(fā)送數(shù)據(jù)開始,到發(fā)送端收到來自接收端的確認(rèn),總共經(jīng)歷的時延
    • 通常 1 個 HTTP 的數(shù)據(jù)包在 14KB 左右
      • 首先是請求 HTML 資源,假設(shè)大小是 6KB,小于 14KB,所以 1 個 RTT 就可以解決
    • 至于 JavaScriptCSS 文件
      • 由于渲染引擎有一個預(yù)解析的線程,在接收到 HTML 數(shù)據(jù)之后,預(yù)解析線程會快速掃描 HTML 數(shù)據(jù)中的關(guān)鍵資源,一旦掃描到了,會立馬發(fā)起請求
      • 可以認(rèn)為 JavaScriptCSS同時發(fā)起請求的,所以它們的請求是重疊的,計(jì)算它們的 RTT 時,只需要計(jì)算體積最大的那個數(shù)據(jù)就可以了
  • <span style="font-weight:800;color:#FFA500;font-size:18px">{關(guān)鍵字節(jié)| Critical Bytes}</span>:作為完成和構(gòu)建頁面的一部分而傳輸?shù)?strong>字節(jié)總數(shù)。

在我們的第一個例子中,如果是普通的HTML腳本,上面各個指標(biāo)的值如下

  • 1個關(guān)鍵資源(html)
  • 1個RTT
  • 192字節(jié)的數(shù)據(jù)

在第二個例子中,一個普通的HTML和外部CSS腳本,上面各個指標(biāo)的值如下

  • 2個關(guān)鍵資源(html+css)
  • 2個RTT
  • 400字節(jié)的數(shù)據(jù)

如果你希望優(yōu)化任何框架中的關(guān)鍵渲染路徑,你需要在上述指標(biāo)上下功夫并加以改進(jìn)。

  • 優(yōu)化關(guān)鍵資源
    • JavaScriptCSS 改成內(nèi)聯(lián)的形式 (性能提升不是很大)
    • 如果 JavaScript 代碼沒有 DOM 或者 CSSOM 的操作,則可以改成 sync 或者 defer 屬性
    • 首屏內(nèi)容可以優(yōu)先加載,非首屏內(nèi)容采用滾動加載
  • 優(yōu)化關(guān)鍵路徑長度
    • 壓縮 CSSJavaScript 資源
    • 移除 HTML、CSS、JavaScript 文件中一些注釋內(nèi)容
  • 優(yōu)化關(guān)鍵字節(jié)
  • 通過減少關(guān)鍵資源的個數(shù)和減少關(guān)鍵資源的大小搭配來實(shí)現(xiàn)
  • 使用 CDN 來減少每次 RTT 時長

減少渲染器阻塞資源

懶加載

加載的關(guān)鍵是 "懶加載"。任何媒體資源、CSS、JavaScript、圖像、甚至HTML都可以被懶加載。每次加載有限的頁面的內(nèi)容,可以提高關(guān)鍵渲染路徑。

  • 不要在加載頁面時加載這個整個頁面的 CSS、JavaScriptHTML。
  • 相反,可以為一個button添加一個事件監(jiān)聽,只有在用戶點(diǎn)擊按鈕時才加載腳本。
  • 使用Webpack來完成懶加載功能。

這里有一些利用純JavaScript實(shí)現(xiàn)懶加載的技術(shù)。

比如,現(xiàn)在又一個<img/>/<iframe/> 在這些情況下,我們可以利用<img><iframe>標(biāo)簽附帶的默認(rèn)loading屬性。當(dāng)瀏覽器看到這個標(biāo)簽時,它會推遲加載iframeimage。具體語法如下:

<img src="image.png" loading="lazy">
<iframe src="abc.html" loading="lazy"></iframe>

注意:loading=lazy的懶加載不應(yīng)該用在非滾動視圖上。

不能利用loading=lazy的瀏覽器中,你可以使用IntersectionObserver。這個API設(shè)置了一個根,并為每個元素的可見性配置了根的比率。當(dāng)一個元素在視口中是可見的,它就會被加載。

IntersectionObserverEntry 對象提供目標(biāo)元素的信息,一共有六個屬性。
每個屬性的含義如下。

  • time:可見性發(fā)生變化的時間,是一個高精度時間戳,單位為毫秒
  • target:被觀察的目標(biāo)元素,是一個 DOM 節(jié)點(diǎn)對象
  • rootBounds:根元素的矩形區(qū)域的信息,getBoundingClientRect()方法的返回值,如果沒有根元素(即直接相對于視口滾動),則返回null
  • boundingClientRect:目標(biāo)元素的矩形區(qū)域的信息
  • intersectionRect:目標(biāo)元素與視口(或根元素)的交叉區(qū)域的信息
  • intersectionRatio:目標(biāo)元素的可見比例,即intersectionRectboundingClientRect的比例,完全可見時為1,完全不可見時小于等于0
  • 我們觀察所有具有.lazy類的元素。
  • 當(dāng)具有.lazy類的元素在視口上時,相交率會降到零以下。如果相交率為零或低于零,說明目標(biāo)不在視口內(nèi)。而且,不需要做什么。
var intersectionObserver = new IntersectionObserver(function(entries) {
  if (entries[0].intersectionRatio <= 0) return;

  //intersection ratio 在0上,說明在視口上能看到
  console.log('進(jìn)行加載處理');
});
// 針對目標(biāo)DOM進(jìn)行處理
intersectionObserver.observe(document.querySelector('.lazy));

Async, Defer, Preload

注意AsyncDefer 是用于外部腳本的屬性。

使用Async處理腳本

當(dāng)使用 Async 時,將允許瀏覽器在下載 JavaScript 資源時做其他事情。一旦下載完成,下載的JavaScript資源將被執(zhí)行。

  1. JavaScript異步下載的。
  2. 所有其他腳本的執(zhí)行將被暫停。
  3. DOM渲染將同時發(fā)生。
  4. DOM渲染將只在腳本執(zhí)行時暫停。
  5. 渲染阻塞的JavaScript問題可以使用async屬性來解決。

如果一個資源不重要,甚至不要使用async,完全省略它

<p>...執(zhí)行腳本之前,能看到的內(nèi)容...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM 被構(gòu)建完成!"));
</script>

<script async src=""></script>

<p>...上述腳本執(zhí)行完,才能看到此內(nèi)容 ...</p>

使用Defer處理腳本

當(dāng)使用Defer時,JavaScript 資源將在HTML渲染時被下載。然而,執(zhí)行不會在腳本被下載后立即發(fā)生。相反,它會等待HTML文件被完全渲染。

  1. 腳本的執(zhí)行只發(fā)生在渲染完成之后。
  2. Defer 可以使你的JavaScript資源絕對不會阻斷渲染
<p>...執(zhí)行腳本之前,能看到的內(nèi)容...</p>

<script defer src=""></script>

<p>...此內(nèi)容不被js所阻塞,也就是說能立即看到...</p>

使用Prelaod處理外部資源

當(dāng)使用Preload時,它被用于HTML文件中沒有的文件,但在渲染或解析JavaScript或CSS文件的時候。有了Preload,瀏覽器就會下載資源,在資源可用的時候就會執(zhí)行。

  • 使用Prelaod。瀏覽器會下載文件,即使它在你的頁面上是不必要的。
  • 太多的預(yù)載會使你的頁面速度下降。
  • 當(dāng)有太多的預(yù)載文件時,使用預(yù)載的固有優(yōu)先權(quán)將受到影響。
  • 只有在首屏頁面需要的文件才可以預(yù)載
  • 預(yù)載文件會在其他文件被渲染時才會被發(fā)現(xiàn)。例如,你在一個CSS文件內(nèi)添加一個字體的引用。在CSS文件被解析之前,對字體的存在不會被知道。如果該字體被提前下載,它將提高你的網(wǎng)站速度。
  • 預(yù)加載只用于<link>標(biāo)簽。
<link rel="preload" href="style.css" as="style">
<link rel="preload" href="main.js" as="script">

編寫原生(Vanilla) JS,避免使用第三方腳本

原生 JS擁有很好的性能和可訪問性。對于一個特定的用例,你不需要全盤的依賴第三方腳本。雖然這些庫往往能解決一堆問題,但是依靠沉重的庫來解決簡單的問題會導(dǎo)致你的代碼性能下降。

我們的要求不是避免使用框架和編寫100%的新代碼。我們的要求是使用輔助函數(shù)和小規(guī)模的插件。


<span style="font-weight:800;color:#FFA500;font-size:18px">{緩存| Caching}</span>和<span style="font-weight:800;color:#FFA500;font-size:18px">{失效| Expiring}</span>內(nèi)容

如果資源在你的頁面上被反復(fù)使用,那么一直加載它們將是一種折磨。這類似于每次都在加載網(wǎng)站。緩存將有助于防止這種循環(huán)。在HTTP響應(yīng)頭中給內(nèi)容提供過期信息,只有在它們過期時才加載。

HTTP緩存

我們之前在網(wǎng)絡(luò)拾遺之Http緩存就介紹過,關(guān)于http緩存的知識點(diǎn),我就直接拿來主義了。

最好最快的請求就是沒有請求

瀏覽器對靜態(tài)資源的緩存本質(zhì)上是 HTTP 協(xié)議的緩存策略,其中又可以分為強(qiáng)制緩存協(xié)商緩存。

兩種緩存策略都會將資源緩存到本地

  • 強(qiáng)制緩存策略根據(jù)過期時間決定使用本地緩存還是請求新資源:
  • 協(xié)商緩存每次都會發(fā)出請求,經(jīng)過服務(wù)器進(jìn)行對比后決定采用本地緩存還是新資源。

具體采用哪種緩存策略,由 HTTP 協(xié)議的首部( Headers )信息決定。

網(wǎng)絡(luò)通信之生成HTTP消息中我們介紹過,消息頭按照用途可分為四大類
1. 通用頭:適用于請求和響應(yīng)的頭字段
2. 請求頭:用于表示請求消息的附加信息的頭字段
3. 響應(yīng)頭:用于表示響應(yīng)消息的附加信息的頭字段
4. 實(shí)體頭:用于消息體的附加信息的頭字段

我們對HTTP緩存用到的字段進(jìn)行一次簡單的分類和匯總。

頭字段 所屬分組
Expires 實(shí)體頭
Cache-control 通用頭
ETag 實(shí)體頭

ETag: 在更新操作中,有時候需要基于上一次請求的響應(yīng)數(shù)據(jù)來發(fā)送下一次請求。在這種情況下,這個字段可以用來提供上次響應(yīng)與下次請求之間的關(guān)聯(lián)信息。上次響應(yīng)中,服務(wù)器會通過 Etag 向客戶端發(fā)送一個唯一標(biāo)識,在下次請求中客戶端可以通過 If-MatchIf-None-Match、If-Range 字段將這個標(biāo)識告知服務(wù)器,這樣服務(wù)器就知道該請求和上次的響應(yīng)是相關(guān)的。

這個字段的功能和 Cookie 是相同的,但 Cookie 是網(wǎng)景(Netscape)公司自行開發(fā)的規(guī)格,而 Etag 是將其進(jìn)行標(biāo)準(zhǔn)化后的規(guī)格

Expires 和 Cache-control:max-age=x(強(qiáng)緩存)

ExpiresCache-control:max-age=x強(qiáng)制緩存策略的關(guān)鍵信息,兩者均是響應(yīng)首部信息(后端返給客戶端)的。

ExpiresHTTP 1.0 加入的特性,通過指定一個明確的時間點(diǎn)作為緩存資源的過期時間,在此時間點(diǎn)之前客戶端將使用本地緩存的文件應(yīng)答請求,而不會向服務(wù)器發(fā)出實(shí)體請求。

Expires 的優(yōu)點(diǎn):

  • 可以在緩存過期時間內(nèi)減少客戶端的 HTTP 請求
  • 節(jié)省了客戶端處理時間和提高了 Web 應(yīng)用的執(zhí)行速度
  • 減少了服務(wù)器負(fù)載以及客戶端網(wǎng)絡(luò)資源的消耗

對應(yīng)的語法

Expires: <http-date>

<http-date>是一個 HTTP-日期 時間戳

Expires: Wed, 24 Oct 2022 14:00:00 GMT

上述信息指定對應(yīng)資源的緩存過期時間2022年8月24日 14點(diǎn)

Expires 一個致命的缺陷是:它所指定的時間點(diǎn)是以服務(wù)器為準(zhǔn)的時間,但是客戶端進(jìn)行過期判斷時是將本地的時間與此時間點(diǎn)對比。

如果客戶端的時間與服務(wù)器存在誤差,比如服務(wù)器的時間是 2022年 8月 23日 13 點(diǎn),而客戶端的時間是 2022年 8月 23日 15 點(diǎn),那么通過 Expires 控制的緩存資源將會失效,客戶端將會發(fā)送實(shí)體請求獲取對應(yīng)資源。

針對這個問題, HTTP 1.1 新增了 Cache-control 首部信息以便更精準(zhǔn)地控制緩存。

常用的 Cache-control 信息有以下幾種。

  • no-cache:
    使用 ETag 響應(yīng)頭來告知客戶端(瀏覽器、代理服務(wù)器)這個資源首先需要被檢查是否在服務(wù)端修改過,在這之前不能被復(fù)用。這個意味著no-cache將會和服務(wù)器進(jìn)行一次通訊,確保返回的資源沒有修改過,如果沒有修改過,才沒有必要下載這個資源。反之,則需要重新下載。

  • no-store
    在處理資源不能被緩存和復(fù)用的邏輯的時候與 no-cache類似。然而,他們之間有一個重要的區(qū)別。no-store要求資源每次都被請求并且下載下來。當(dāng)在處理隱私信息(private information)的時候,這是一個重要的特性。

  • public & private
    public表示此響應(yīng)可以被瀏覽器以及中間緩存器無限期緩存,此信息并不常用,常規(guī)方案是使用 max-age 指定精確的緩存時間
    private表示此響應(yīng)可以被用戶瀏覽器緩存,但是不允許任何中間緩存器對其進(jìn)行緩存。 例如,用戶的瀏覽器可以緩存包含用戶私人信息的 HTML 網(wǎng)頁,但 CDN 卻不能緩存。

  • max-age=<seconds>
    指定從請求的時刻開始計(jì)算,此響應(yīng)的緩存副本有效的最長時間(單位:) 例如,max-age=360表示瀏覽器在接下來的 1 小時內(nèi)使用此響應(yīng)的本地緩存,不會發(fā)送實(shí)體請求到服務(wù)器

  • s-maxage=<seconds>
    s-maxagemax-age類似,這里的s代表共享,這個指令一般僅用于 CDNs 或者其他中間者(intermediary caches)。這個指令會覆蓋max-ageexpires響應(yīng)頭。

  • no-transform
    中間代理有時會改變圖片以及文件的格式,從而達(dá)到提高性能的效果。no-transform指令告訴中間代理不要改變資源的格式

max-age 指定的是緩存的時間跨度,而非緩存失效的時間點(diǎn),不會受到客戶端與服務(wù)器時間誤差的影響。

Expires 相比, max-age 可以更精確地控制緩存,并且比 Expires 有更高的優(yōu)先級

強(qiáng)制緩存策略下( Cache-control 未指定 no-cache
no-store)的緩存判斷流程


EtagIf-None-Match (協(xié)商緩存)

Etag服務(wù)器為資源分配的字符串形式唯一性標(biāo)識,作為響應(yīng)首部信息返回給瀏覽器

瀏覽器Cache-control 指定 no-cache 或者 max-ageExpires 均過期之后,將Etag 值通過 If-None-Match 作為請求首部信息發(fā)送給服務(wù)器。

服務(wù)器接收到請求之后,對比所請求資源的 Etag 值是否改變,如果未改變將返回 304 Not Modified,并且根據(jù)既定的緩存策略分配新的 Cache-control 信息;如果資源發(fā)生了改變,則會
返回最新的資源以及重新分配Etag值。

如果強(qiáng)制瀏覽器使用協(xié)商緩存策略,需要將 Cache-control 首部信息設(shè)置為 no-cache ,這樣便不會判斷 max-ageExpires 過期時間,從而每次資源請求都會經(jīng)過服務(wù)器對比。


JS層面做緩存處理(ServerWorker)

在純JavaScript中,你可以自由地利用service workers來決定是否需要加載數(shù)據(jù)。例如,我有兩個文件:style.cssscript.js。我需要加載這些文件,我可以使用service workers來決定這些資源是否必須保持最新,或者可以使用緩存。

Web性能優(yōu)化之Worker線程(上)我們有介紹過關(guān)于ServerWork的詳細(xì)介紹。如果感興趣,可以去瞅瞅。

當(dāng)用戶第一次啟動單頁應(yīng)用程序時,安裝將被執(zhí)行

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open(cacheName).then(function(cache) {
      return cache.addAll(
        [
          'styles.css',
          'script.js'
        ]
      );
    })
  );
});

當(dāng)用戶執(zhí)行一項(xiàng)操作時

document.querySelector('.lazy').addEventListener('click', function(event) {
  event.preventDefault();
  caches.open('lazy_posts’).then(function(cache) {
    fetch('/get-article’).then(function(response) {
      return response;
    }).then(function(urls) {
      cache.addAll(urls);
    });
  });
});

處理網(wǎng)絡(luò)請求

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open('lazy_posts').then(function(cache) {
      return cache.match(event.request).then(function (response) {
        return response 
      });
    })
  );
});

紙上得來終覺淺,絕知此事要躬行。道理,都懂,我們來看看在實(shí)際開發(fā)中,如何做優(yōu)化處理。我們按React開發(fā)為例子。

React 應(yīng)用中的優(yōu)化處理

優(yōu)化被分成兩個階段。

    1. 在應(yīng)用程序被加載之前
    1. 第二階段是在應(yīng)用加載后進(jìn)行優(yōu)化

階段一(加載前)

讓我們建立一個簡單的應(yīng)用程序,有如下的結(jié)構(gòu)。

  • Header
  • Sidebar
  • Footer

代碼結(jié)構(gòu)如下。

webpack-demo
|- package.json
|- package-lock.json
|- webpack.config.js
|- /dist
|- /src
 |- index.js
 |- Header.js
 |- Sidebar.js
 |- Footer.js
 |- loader.js
 |- route.js
|- /node_modules

在我們的應(yīng)用程序中,只有當(dāng)用戶登錄時,才應(yīng)該看到側(cè)邊欄。Webpack 是一個很好的工具,可以幫助我們進(jìn)行代碼拆分。如果我們啟用了代碼拆分,我們可以從App.jsRoute組件對 React進(jìn)行 Lazy加載處理。

我們把代碼按頁面邏輯進(jìn)行區(qū)分。只有當(dāng)應(yīng)用程序需要時,才會加載這些邏輯片段。因此,代碼的整體重量保持較低。

例如,如果Sidebar組件只有在用戶登錄時才會被加載,我們有幾個方法來提高我們的應(yīng)用程序的性能。

首先,我們可以在路由層面對代碼進(jìn)行懶加載處理。如下面代碼所示,代碼被分成了三個邏輯塊。只有當(dāng)用戶選擇了一個特定的路由時,每個塊才會被加載。這意味著,我們的DOM在初始繪制時不必將 Sidarbar 代碼作為其 Critical Bytes的一部分。

import { 
    Switch, 
    browserHistory, 
    BrowserRouter as Router, 
    Route
} from 'react-router-dom';
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import('Footer'));
const Sidebar = React.lazy( () => import('Sidebar'));

const Routes = (props) => {
  return isServerAvailable ? (
      <Router history={browserHistory}>
         <Switch>
           <Route path="/" exact><Redirect to='/Header' /></Route>
           <Route path="/sidebar" exact component={props => <Sidebar {...props} />} />
           <Route path="/footer" exact component={props => <Footer {...props} />} />
        </Switch>
      </Router>
}

同樣地,我們也可以從父級App.js中實(shí)現(xiàn)懶加載。這利用了React條件渲染機(jī)制。

const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import('Footer'));
const Sidebar = React.lazy( () => import('Sidebar'));

function App (props) {
  return(
    <React.Fragment>
       <Header user = {props.user} />
       {props.user ? <Sidebar user = {props.user /> : null}
       <Footer/>
    </React.Fragment>
  )
}

談到條件渲染,React 允許我們在點(diǎn)擊按鈕的情況下也能加載組件。

import _ from 'lodash';
function buildSidebar() {
   const element = document.createElement('div');
   const button = document.createElement('button');
   button.innerHTML = '登錄';
   element.innerHTML = _.join(['加載 Sidebar', 'webpack'], ' ');
   element.appendChild(button);
   button.onclick = e => 
       import(/* webpackChunkName: "sidebar" */ './sidebar')
       .then(module => {
         const sidebar = module.default;
         sidebar()   
       });

   return element;
 }

document.body.appendChild(buildSidebar());

在實(shí)踐中,重要的是把所有的路由或組件寫在在叫做Suspense的組件中,以懶加載的方式加載。Suspense 的作用是在懶加載的組件被加載時,為應(yīng)用程序提供一個后備內(nèi)容。后備內(nèi)容可以是任何東西,比如一個<Loader/>,或者一條消息,告訴用戶為什么頁面還沒有被畫出來。

import React, { Suspense } from 'react';
import { 
    Switch, 
    browserHistory, 
    BrowserRouter as Router, 
    Route
} from 'react-router-dom';
import Loader from ‘./loader.js’
const Header = React.lazy( () => import('Header'));
const Footer = React.lazy( () => import('Footer'));
const Sidebar = React.lazy( () => import('Sidebar'));

const Routes = (props) => {
return isServerAvailable ? (
<Router history={browserHistory}>
    <Suspense fallback={<Loader trigger={true} />}>
         <Switch>
           <Route path="/" exact><Redirect to='/Header' /></Route>
           <Route path="/sidebar" exact component={props => <Sidebar {...props} />} />
           <Route path="/footer" exact component={props => <Footer {...props} />} />
         </Switch>
    </Suspense>
</Router>
}


階段二

現(xiàn)在,應(yīng)用程序已經(jīng)完全加載,接下來就到了調(diào)和階段了。其中的所有的處理邏輯都是React為我們代勞。其中最重要的一點(diǎn)就是React-Fiber機(jī)制。

如果想了解React_Fiber,可以參考我們之前的文章。

使用正確的狀態(tài)管理方法

  • 每當(dāng)React DOM樹被修改時,它都會迫使瀏覽器回流。這將對你的應(yīng)用程序的性能產(chǎn)生嚴(yán)重影響。調(diào)和被用來確保減少重新流轉(zhuǎn)的次數(shù)。同樣地,React使用狀態(tài)管理來防止重現(xiàn)。例如,你有一個useState()hook。
  • 如果使用的是類組件,利用shouldComponentUpdate()生命周期方法。shouldComponentUpdate()必須在PureComponent中實(shí)現(xiàn)。當(dāng)你這樣做時,stateprops之間會發(fā)生淺對比。因此,重新渲染的幾率大大降低。

利用React.Memo

  • React.Memo接收組件,并將props記憶化。當(dāng)一個組件需要重新渲染時,會進(jìn)行淺對比。由于性能原因,這種方法被廣泛使用。
function MyComponent(props) {
}
function areEqual(prevProps, nextProps) {
  //對比nextProps和prevProps,如果相同,返回false,不會發(fā)生渲染
  // 如果不相同,則進(jìn)行渲染
}
export default React.memo(MyComponent, areEqual);
  • 如果使用函數(shù)組件,請使用useCallback()useMemo()

后記

分享是一種態(tài)度。

參考資料:

全文完,既然看到這里了,如果覺得不錯,隨手點(diǎn)個贊和“在看”吧。

本文由mdnice多平臺發(fā)布

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容