瀏覽器前端優化

瀏覽器前端優化

優化全都是與速度和滿意度有關。

  • 從用戶體驗的角度,我們希望前端提供可以快速加載和執行的網頁。
  • 而從開發者體驗的角度,我們希望前端是快速、簡單而規範的。

這不僅會給我們帶來快樂的用戶和快樂的開發者,更多IT行業幹活鎖定公眾號“朗妹兒”獲取更多資源而且由於 Google 偏向於優化,SEO 排名也會顯著提高。

如果你已經花費了大量時間來改善你網站的 Google Pagespeed Insights分數,那麼這將有助於揭示這一切實際上意味著什麼,以及我們必須為優化前端所採取的大量策略。

背景

最近我的整個團隊有機會花一些時間加快把我們提出的升級變為代碼庫,可能是用 React。這確實讓我思考起了我們該如何創建前端。很快,我意識到瀏覽器將是我們的方法中的一個重要因素,同時也是我們知識中的大瓶頸。

方法

首先

我們不能控制瀏覽器或者改變它的行為方式,但是我們可以理解它的工作原理,這樣就可以優化我們提供的負載。

幸運的是,瀏覽器行為的基礎原理是相當穩定而且文檔齊全的,並且在相當長一段時間內肯定不會發生顯著變化。

所以這至少給了我們一個目標。

其次

另一方面,代碼、技術棧、架構和模式是我們可以控制的東西。它們更靈活,變化的更快,並給我們這一邊提供了更多選擇。

因此

我決定從外向內著手,搞清楚我們代碼的最終結果應該是什麼樣的,然後形成編寫代碼的意見。在這第一篇博文中,我們打算專注於對於瀏覽器來說我們需要了解的一切。

瀏覽器都做了什麼

下面我們來建立一些知識。如下是我們希望瀏覽器要運行的一些簡單 HTML:




The "Click the button" page





Click the button.






html

瀏覽器如何渲染網頁

當瀏覽器接收到 HTML 時,就會解析它,將其分解為瀏覽器所能理解的詞彙,而這個詞彙由於HTML5 DOM 規範定義,在所有瀏覽器中是保持一致的。然後瀏覽器通過一系列步驟來構造和渲染頁面。如下是一個很高層次的概述:

  1. 使用 HTML 創建文檔對象模型(DOM)。
  2. 使用 CSS 創建 CSS 對象模型(CSSOM)。
  3. 基於 DOM 和 CSSOM 執行腳本(Script)
  4. 合併 DOM 和 CSSOM 形成渲染樹(Render Tree)。
  5. 使用渲染樹佈局(Layout)所有元素的大小和位置。
  6. 繪製(Paint)所有元素。
瀏覽器前端優化

步驟一 — HTML

瀏覽器開始從上到下讀取標記,並且通過將它分解成節點,來創建 DOM 。

瀏覽器前端優化

HTML 加載優化策略

  • 樣式在頂部,腳本在底部

雖然這個規則有例外和細微差別,不過總體思路是儘可能早地加載樣式,儘可能晚地加載腳本。原因是腳本執行之前,需要 HTML 和 CSS 解析完成,因此,樣式儘可能的往頂部放,這樣在底部腳本開始編譯和執行之前,樣式有足夠的時間完成計算。

下面我們進一步研究如何在優化的同時做細微調整。

  • 最小化和壓縮

這適用於我們提交的所有內容,包括 HTML、CSS、JavaScript、圖片和其它資源。

最小化是移除所有多餘的字符,包括空格、註釋、多餘的分號等等。

壓縮(比如 GZip)是將代碼或者資源中重複的數據替換為一個指向原始實例的指針,大大壓縮下載文件的大小,並且是在客戶端解壓文件。

雙管齊下的話,可以潛在將負載降低 80% 到 90%。比如:光 bootstrap 就節省了 87% 的負載。

  • 可訪問性

雖然可訪問性不會讓頁面的下載變得更快,但是會大大提高殘障人士的滿意度。要確保是給所有人提供的!給元素加上 aria 標籤,給圖片提供 alt文本,以及所有其它好東西。

使用像 WAVE 這樣的工具確認在哪些地方可以改善可訪問性。

步驟二 — CSS

當瀏覽器發現任何與節點相關的樣式時(即外部樣式表、內部樣式表或行內樣式),就立即停止渲染 DOM ,並用這些節點來創建 CSSOM。這就是人們稱 CSS 阻塞渲染的原因。這裡是不同類型樣式的一些優缺點。

//外部樣式

// 內部樣式

// 行內樣式

xml

CSSOM 節點的創建與 DOM 節點的創建類似,隨後,兩者會被合併。這就是現在它們的樣子:

瀏覽器前端優化

CSSOM 的構建會阻塞頁面的渲染,因此我們想在樹中儘可能早地加載樣式,讓它們儘可能輕便,並且在有效的地方延遲加載它們。

CSS 加載優化策略

  • 使用 media 屬性

media 屬性指定要加載樣式必須滿足的條件,比如:是最大還是最小分辨率?是面向屏幕閱讀器嗎?

桌面是很強大,但是移動設備不是,所以我們想給移動設備儘可能最輕的負載。我們可以假設先只提供移動端樣式,然後對桌面端樣式放一個媒體條件。雖然這樣做不會阻止桌面端樣式下載,不過會阻止它阻塞頁面加載和使用寶貴的資源。

// 這個 css 在所有情況都會下載,並阻塞頁面的渲染。
// media="all" 是默認值,並且與不聲明任何 media 屬性一樣。

// 在移動端,這個 css 會在後臺下載,而且不會中斷頁面加載。

// CSS 中只為打印視圖計算的媒體查詢


xml
  • 延遲加載 CSS

如果我們有一些樣式可以等到首屏有價值的內容渲染完成後,再加載和計算,比如出現在首屏以下的,或者頁面變成響應式之後不那麼重要的東西。我們可以把樣式加載寫在腳本中,用腳本等待頁面加載完成,然後再插入樣式。







摺疊內容之上的重要部分。


摺疊內容之下。頁面加載之後,向下滾動才會看到的東西。




xml

這裡有一個如何實現這個的例子,還有另一個例子。

  • 較小的特殊性

鏈在一起的元素越多,自然要傳輸的數據就越多,因而會增大 CSS 文件,這是一個明顯的缺點。不過這樣做還有一個客戶端計算的損耗,即會把樣式計算為較高的特殊性。

// 更具體的選擇器 == 糟糕
.header .nav .menu .link a.navItem {
font-size: 18px;
}
// 較不具體的選擇器 == 好
a.navItem {
font-size: 18px;
}
stylus
  • 只加載需要的樣式

這聽起來可能有點愚蠢或者裝模作樣,不過如果你已經從事前端工作多年的話,就會知道 CSS 的一個最大問題是刪除東西的不可預測性。設計的時候它就是被下了不斷增長這樣的詛咒。

要儘可能多削減 CSS ,可以使用類似uncss)包這樣的工具,或者如果想有一個網上的替代品,那就到處找找,還是有挺多選擇的。

步驟三 — JavaScript

然後,瀏覽器會不斷創建 DOM / CSSOM 節點,直到發現任何 JavaScript 節點,即外部或者行內的腳本。

// 外部腳本

// 內部腳本

xml

由於腳本可能需要訪問或操作之前的 HTML 或樣式,我們必須等待它們構建完成。

因此瀏覽器必須停止解析節點,完成創建 CSSOM,執行腳本,然後再繼續。這就是人們稱 JavaScript 阻塞解析器的原因。

瀏覽器有種稱為'預加載掃描器'的東西,它會掃描 DOM 的腳本,並開始預加載腳本,不過腳本只會在先前的 CSS 節點已經構建完成後,才會依次執行。

假如這就是我們的腳本:

var button = document.querySelector("button");
button.style.fontWeight = "bold";
button.addEventListener("click", function () {
alert("Well done.");
});
mipsasm

那麼這就是我們的 DOM 和 CSSOM 的效果:

瀏覽器前端優化

JavaScript 加載優化策略

優化腳本是我們可以做的最重要的事情之一,同時也是大多數網站做得最糟糕的事情之一。

  • 異步加載腳本

通過在腳本上使用 async 屬性,可以通知瀏覽器繼續,用另一個低優先級的線程下載這個腳本,而不要阻塞其餘頁面的加載。一旦腳本下載完成,就會執行。


這意味著這段腳本可以隨時執行,這就導致了兩個明顯的問題。首先,它可以在頁面加載後執行很長時間,所以如果依靠它為用戶體驗做一些事情,那麼可能會給用戶一個不是最佳的體驗。其次,如果它剛好在頁面完成加載之前執行,我們就沒法預測它會訪問正確的 DOM/CSSOM 元素,並且可能會中斷執行。

瀏覽器前端優化

async 適用於不影響 DOM 或 CSSOM 的腳本,而且尤其適用於與 HTML 和 CSS 代碼無關,對用戶體驗無影響的外部腳本,比如分析或者跟蹤腳本。不過如果你發現了任何好的使用案例,那就用它。

  • 延遲加載腳本

defer 跟 async 非常相似,不會阻塞頁面加載,但會等到 HTML 完成解析後再執行,並且會按出現的次序執行。

瀏覽器前端優化

這對於會作用於渲染樹上的腳本來說,確實是一個好的選擇。不過對於加載摺疊內容之上的頁面,或者需要之前的腳本已經運行的腳本來說,並不重要。


這裡是使用 defer 策略的另一個好選擇,或者也可以使用 addEventListener。如果想了解更多,請從這裡開始閱讀。

// 所有對象都在 DOM 中,並且所有圖像、腳本、link 和子幀都完成了加載。
window.onload = function () {
};
// 在 DOM 準備好時調用,在圖像和其它外部內容準備好之前
document.onload = function () {
};
// JQuery 的方式
$(document).ready(function () {
});
javascript

不幸的是 async 和 defer 對於行內腳本不起作用,因為只要有行內腳本,瀏覽器默認就會編譯和執行它們。當腳本內嵌在 HTML 中時,就會立即運行,通過在外部資源上使用上述兩個屬性,我們只是把腳本抽取出來,或者延遲把腳本發佈到 DOM/CSSOM。

  • 操作之前克隆節點

當且僅當對 DOM 執行多次修改時看到了不必要的行為時,就試試這招。

先克隆整個 DOM 節點,對克隆的節點做修改,然後用它來替換原始節點,這樣可能效率更高。因為這樣就避免了多次重畫,降低了 CPU 和內存消耗。這樣做還可以避免更改頁面時的抖動和無樣式內容的閃爍(Flash of Unstyled Content,FOUC)。

// 通過克隆,高效操作一個節點
var element = document.querySelector(".my-node");
var elementClone = element.cloneNode(true); // (true) 也克隆子節點, (false) 不會
elementClone.textContent = "I've been manipulated...";
elementClone.children[0].textContent = "...efficiently!";
elementClone.style.backgroundColor = "green";
element.parentNode.replaceChild(elementClone, element);
nix

請注意,克隆的時候並沒有克隆事件監聽器。有時這實際上剛好是我們想要的。過去我們已經用過這種方法來重置不調用命名函數時的事件監聽器,而且那時也沒有 jQuery 的 .on() 和 .off() 方法可用。

  • Preload/Prefetch/Prerender/Preconnect

這些屬性基本上也實現了它們所做的承諾,而且都棒極了。不過,這些屬性都是相當新,還沒被瀏覽器普遍支持,也就是說對我們大多數人來說它們實際上不是真正的競爭者。不過,如果你有空的話,可以看看這裡和這裡。

步驟四 — 渲染樹

一旦所有節點已被讀取,DOM 和 CSSOM 準備好合併,瀏覽器就會構建渲染樹。如果我們把節點當成單詞,把對象模型當成句子,那麼渲染樹便是整個頁面。現在瀏覽器已經有了渲染頁面所需的所有東西。

瀏覽器前端優化

步驟五 — 佈局

然後我們進入佈局階段,確定頁面上所有元素的大小和位置。

瀏覽器前端優化

步驟六 — 繪製

最終我們進入繪製階段,真正地光柵化屏幕上的像素,為用戶繪製頁面。

瀏覽器前端優化

整個過程通常會在幾秒或者十分之一秒內發生。我們的任務是讓它做得更快。

如果 JavaScript 事件改變了頁面的某個部分,就會導致渲染樹的重繪,並且迫使我們再次經歷佈局和繪製。現在瀏覽器足夠智能,會僅進行部分重畫。不過我們不能指望靠這就能高效或者高性能。

話雖如此,不過很顯然 JavaScript 主要是在客戶端基於事件,而且我們想讓它來操作 DOM,所以它就得做到這一點。我們只是必須限制它的不良影響。

至此你已經足夠認識到要感謝 Tali Garsiel 的演講。這是 2012 年的演講,但是信息依然是真實的。她在此主題上的綜合論文可以在這裡讀到。

如果你喜歡迄今為止所讀過的內容,但仍然渴望知道更多的底層技術性的東西,那麼所有知識的權威就是HTML5 規範。

我們差不多搞定了,不過請和我多待段時間!現在我們來探討為什麼需要知道上面的所有知識。

瀏覽器如何發起網絡請求

本節中,我們將理解如何才能高效地把渲染頁面所需的數據傳輸給瀏覽器。

當瀏覽器請求一個 URL 時,服務器會響應一些 HTML。我們將從超級小的開始,慢慢增加複雜性。

假如這就是我們頁面的 HTML:




The "Click the button" page



Button under construction...




xml

我們需要學習一個新術語,關鍵渲染路徑(Critical Rendering Path,CRP),就是瀏覽器渲染頁面所需的步數。如下就是現在我們的 CRP 示意圖看起來的樣子:

瀏覽器前端優化

瀏覽器發起一個 GET 請求,在我們頁面(現在還沒有 CSS 及 JavaScript)所需的 1kb HTML 響應回來之前,它一直是空閒的。接收到響應之後,它就能創建 DOM,並渲染頁面。

關鍵路徑長度

三個 CRP 指標的第一個是路徑長度。我們想讓這個指標儘可能低。

瀏覽器用一次往返來獲取渲染頁面所需的 HTML,而這就是它所需的一切。因此我們的關鍵路徑長度是 1,很完美。

下面我們上一個檔次,加點內部樣式和內部 JavaScript。




The "Click the button" page







xml

如果我們檢查一下 CRP 示意圖,應該能看到有兩點變化。

瀏覽器前端優化

新增了兩步,創建 CSSOM執行腳本。這是因為我們的 HTML 有內部樣式和內部腳本需要計算。不過,由於沒有發起外部請求,關鍵路徑長度沒變。

但是注意,渲染沒那麼快了。而且我們的 HTML 大小增加到了 2kb,所以某些地方還是受了影響。

關鍵字節數

那就是三個指標之二,關鍵字節數出現的地方。這個指標用來衡量渲染頁面需要傳送多少字節數。並非頁面會下載的所有字節,而是隻是實際渲染頁面,並把它響應給用戶所需的字節。

不用說,我們也想減少這個數。

如果你認為這就不錯了,誰還需要外部資源啊,那就大錯特錯了。雖然這看起來很誘人,但是它在規模上是不可行的。在現實中,如果我的團隊要通過內部或者行內方式給頁面之一提供所需的一切,頁面就會變得很大。而瀏覽器不是為了處理這樣的負載而創建的。

看看這篇關於像 React 推薦的那樣內聯所有樣式時,頁面加載效果的有趣文章。DOM 變大了四倍,掛載花了兩倍的時間,到可以響應多花了 50% 的時間。相當不能接受。

還要考慮一個事實,就是外部資源是可以被緩存的,因此在回訪頁面,或者訪問用相同資源(比如 my-global.css)的其它頁面時,瀏覽器就用發起網絡請求,而是用其緩存的版本,從而為我們贏得更大的勝利。

所以下面我們更進一步,對樣式和腳本使用外部資源。注意這裡我們有一個外部 CSS 文件、一個外部 JavaScript 文件和一個外部 asyncJavaScript 文件。




The "Click the button" page


// async






xml

如下是現在 CRP 示意圖看起來的樣子:

瀏覽器前端優化

瀏覽器得到頁面,創建 DOM,一發現任何外部資源,預加載掃描器就開始介入。繼續,開始下載 HTML 中所找到的所有外部資源。CSS 和 JavaScript 有較高的優先級,其它資源次之。

它挑出我們的 styles.css 和 app.js,開闢另一條關鍵路徑去獲取它們。不過不會挑出 analytics.js,因為我們給它加了 async屬性。瀏覽器依然會用另一個低優先級的線程下載它,不過因為它不會阻塞頁面渲染,所以也與關鍵路徑無關。這正是 Google 自己的優化算法對網站進行排名的方式。

關鍵文件

最後,是我們最後一個 CRP 指標,關鍵文件,也就是瀏覽器渲染頁面需要下載的文件總數。在例三中,HTML 文件本身、CSS 和 JavaScript 文件都是關鍵文件。async 的腳本不算。當然,文件越少越好。

回到關鍵路徑長度

現在你可能會認為這肯定就是最長的關鍵路徑吧?我的意思是說要渲染頁面,我們只需要下載 HTML、CSS 和 JavaScript,而且只需要兩個往返就可以搞定。

HTTP1 文件限制

不過,生活依然沒那麼簡單。拜 HTTP1 協議所賜,我們的瀏覽器一次從一個域名併發下載的最大文件數是有限制的。範圍從 2(很老的瀏覽器)到 10(Edge)或者 6(Chrome)。

你可以從這裡查看用戶瀏覽器請求你的域名時的最大併發文件數。

你可以並且應該通過把一些資源放在影子域名上,來繞開這個限制,從而最大限度地提高優化潛力。

警告:不要把關鍵的 CSS 放到根域名之外的其他地方,DNS 查找和延遲都會抵消這樣做時所帶來的任何可能的好處。

HTTP2

如果網站是 HTTP2,並且用戶的瀏覽器也是兼容的,那麼你就可以完全避開這個限制。不過,這種好事並不常見。

可以在這裡測試你網站的 HTTP2。

TCP 往返限制

另一個敵人逼近了!

任何一次往返可傳輸的最大數據量是 14kb,對於包括所有 HTML、CSS 和腳本在內的所有網絡請求都是如此。這來自於防止網絡擁堵和丟包的一個 TCP 規範。

如果一次請求中,我們的 HTML 或者任何累積的資源超過了 14kb,那麼就需要多做一次往返來獲取它們。所以,是的,這些大的資源確實會給 CRP 添加很多路徑。

大招

現在將我們的大網頁傾巢而出。




The "Click the button" page
// 14kb
// 2kb
// 2kb
// 2kb
// 2kb



// 2kb
// 2kb
// 2kb


xml

現在我知道一個按鈕就有很多 CSS 和 JavaScript,但是它是一個很重要的按鈕,它對我們來說意義重大。所以就不要評判,好嗎?

整個頁面被很好地最小化和壓縮到 2kb,遠低於 14kb 的限制,所以我們又回到正好一次 CRP 往返了,而瀏覽器開始忠實地用一個關鍵文件,即我們的 HTML,來創建 DOM。

瀏覽器前端優化

CRP 指標:長度 1,文件數 1,字節數 2kb

瀏覽器發現了一個 CSS 文件,而預加載掃描器識別出所有外部資源(CSS 和 JavaScript),併發送一個請求開始下載它們。但是等一等,第一個 CSS 文件是 14kb,超出了一次往返的最大負載,所以它本身就是一個 CRP。

瀏覽器前端優化

CRP 指標:長度 2,文件數 2,字節數 16kb

然後它繼續下載資源。餘下的資源低於 14kb,所以可以在一次往返中搞定。不過由於總共有 7 個資源,而且我們的網站還沒啟用 HTTP2,而且用的是 Chrome,所以這次往返只能下載 6 個文件。

瀏覽器前端優化

CRP 指標:長度 3,文件數 8,字節數 28kb

現在我們終於可以下載完最終文件,並開始渲染 DOM了。

瀏覽器前端優化

CRP 指標:長度 4,文件數 9,字節數 30kb

我們的 CRP 總共有 30kb 的關鍵資源,在 9 個關鍵文件和 4 個關鍵路徑中。有了這個信息,以及一些關於連接中延遲的知識,我們實際上就可以開始對給定用戶的頁面性能進行真正準確的估計了。

瀏覽器網絡優化策略

  • Pagespeed Insights

使用Insights 來確定性能問題。Chrome DevTools 中還有個 audit標籤。

  • 善用 Chrome 開發者工具

DevTools 如此驚人。我們為它單獨寫一整本書,不過這裡已經有不少資源可以幫助你。這裡)有一篇開始解釋網絡資源的文章值得一讀。

  • 在好的環境中開發,在糟糕的環境中測試

你當然想在你的 1tb SSD、32G 內存的 Macbook Pro 上開發,不過對於性能測試,應該轉到 Chrome 中的 network 標籤下,模擬低帶寬、節流 CPU 連接,從而真正得到一些有用的信息。

  • 合併資源/文件

在上面的 CRP 示意圖中,我省略了一些你不需要知道的東西。不過基本上,每接收到一個外部 CSS 和 JavaScript 文件後,瀏覽器都會構建 CSSOM,並執行腳本。所以,儘管你可以在一次往返中傳遞幾個文件,它們每個也都會讓瀏覽器浪費寶貴的時間和資源,所以最好還是將文件合併在一起,消除不必要的加載。

  • 在 head 部分為首屏設置內部樣式

是讓 CSS 和 JavaScript 內部化或者內聯,以防止獲取外部資源,還是相反,讓資源變成外部資源,這樣就可以緩存,從而讓 DOM 保持輕量,二者並非非黑即白。

但是有一個很好的觀點是對首屏關鍵內容設置內部樣式,可以避免在首次有意義的渲染時獲取資源。

  • 最小化/壓縮圖片

這很簡單、基礎,有很多選擇可以這樣做,選一個你最喜歡的即可。

  • 延遲加載圖片直到頁面加載後

用一些簡單的原生 JavaScript,你就可以延遲加載出現在摺疊部分之下或者對首次用戶響應狀態不重要的圖片。這裡有一些不錯的策略。

  • 異步加載字體

字體加載的代價非常高,如果可以的話,你應該使用帶回退的 web 字體,然後逐步渲染字體和圖標。這看起來可能不咋樣,不過另一個選擇是如果字體還沒有加載,頁面加載時就完全沒有文字,這被稱為不可見文本的閃爍(Flash Of Invisible Text,FOIT)。

  • 是否真正需要 JavaScript/CSS?

你需要嗎?請回答我!是否有原生 HTML 元素可以產生用腳本一樣的行為?是否可以用行內樣式或圖標而不是內部/外部資源?比如,內聯一個 SVG。

  • CDN

內容分發網絡(CDN)可用於為用戶提供物理上更近和更低延遲的位置,從而降低加載時間。


現在你開心慘了,已經知道了足夠多的東西,可以從這裡走出去,自己探索有關這個主題的更多東西了。我推薦參加這個免費的 Udacity 課程,並且閱讀Google 自己的 優化文檔。

如果你渴望更底層的知識,那麼這本免費電子書《高性能瀏覽器網絡》是個開始的好地方。

總結

關鍵渲染路徑是最重要的,它讓網站優化有規律可循。需要關注的 3 個指標是:

1 — 關鍵字節數

2 — 關鍵文件數

3 — 關鍵路徑數

這裡我所寫的應該足以讓你掌握基礎知識,並幫助你解釋 Google Pagespeed Insights對你的性能有什麼看法。

最佳實踐的應用將伴隨著良好的 DOM 結構、網絡優化和可用於減少 CRP 指標的各種策略的結合。讓用戶更高興,讓 Google 的搜索引擎更高興。

在任何企業級網站中,這將是一項艱鉅的任務,但是你必須遲早做到這一點,所以不要再承擔更多的技術性債務,並開始投資于堅實的性能優化策略。

感謝你閱讀至此,如果你真的做到了。衷心希望能幫到你,有任何反饋或者糾正,請給我發消息。


分享到:


相關文章: