瀏覽器前端優化

瀏覽器前端優化
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

本文轉載自:眾成翻譯
譯者:網路埋伏紀事
連結:http://www.zcfy.cc/article/2847
原文:https://hackernoon.com/optimising-the-front-end-for-the-browser-f2f51a29c572#.81dkyz4uu

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

從使用者體驗的角度,我們希望前端提供可以快速載入和執行的網頁。

而從開發者體驗的角度,我們希望前端是快速、簡單而規範的。

這不僅會給我們帶來快樂的使用者和快樂的開發者,而且由於 Google 偏向於優化,SEO 排名也會顯著提高。

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

背景

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

方法

首先

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

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

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

其次

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

因此

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

瀏覽器都做了什麼

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

<!DOCTYPE html>
<html>
<head>
<title>The "Click the button" page</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<h1>
Click the button.
</h1>
<button type="button">Click me</button>
<script>
var button = document.querySelector("button");
button.style.fontWeight = "bold";
button.addEventListener("click", function () {
alert("Well done.");
});
</script>
</body>
</html>

瀏覽器如何渲染網頁

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

使用 HTML 建立文件物件模型(DOM)。

使用 CSS 建立 CSS 物件模型(CSSOM)。

基於 DOM 和 CSSOM 執行指令碼(Script)

合併 DOM 和 CSSOM 形成渲染樹(Render Tree)。

使用渲染樹佈局(Layout)所有元素的大小和位置。

繪製(Paint)所有元素。

步驟一 — HTML

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

HTML 載入優化策略

樣式在頂部,指令碼在底部

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

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

最小化和壓縮

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

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

壓縮(比如 GZip)是將程式碼或者資源中重複的資料替換為一個指向原始例項的指標,大大壓縮下載檔案的大小,並且是在客戶端解壓檔案。

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

可訪問性

雖然可訪問性不會讓頁面的下載變得更快,但是會大大提高殘障人士的滿意度。要確保是給所有人提供的!給元素加上 aria

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

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

CSS 載入優化策略

使用 media 屬性

media 屬性指定要載入樣式必須滿足的條件,比如:是最大還是最小解析度?是面向螢幕閱讀器嗎?

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

// 這個 css 在所有情況都會下載,並阻塞頁面的渲染。
// media="all" 是預設值,並且與不宣告任何 media 屬性一樣。
<link rel="stylesheet" href="mobile-styles.css" media="all">
// 在移動端,這個 css 會在後臺下載,而且不會中斷頁面載入。
<link rel="stylesheet" href="desktop-styles.css" media="min-width: 590px">
// CSS 中只為列印檢視計算的媒體查詢
<style>
@media print {
img {
display: none;
}
}
</style>

延遲載入 CSS

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

<html>
<head>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="main">
摺疊內容之上的重要部分。
</div>
<div class="secondary">
摺疊內容之下。頁面載入之後,向下滾動才會看到的東西。
</div>
<script>
window.onload = function () {
// 載入 secondary.css
}
</script>
</body>
</html>

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

較小的特殊性

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

// 更具體的選擇器 == 糟糕
.header .nav .menu .link a.navItem {
font-size: 18px;
}
// 較不具體的選擇器 == 好
a.navItem {
font-size: 18px;
}

只載入需要的樣式

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

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

步驟三 — JavaScript

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

// 外部指令碼
`<script src="app.js">`</script>
// 內部指令碼
<script>
alert("Oh, hello");
</script>

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

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

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

假如這就是我們的指令碼:

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

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

JavaScript 載入優化策略

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

非同步載入指令碼

通過在指令碼上使用 async

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

async

這裡是使用 defer 策略的另一個好選擇,或者也可以使用 addEventListener

不幸的是 async

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

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

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

關鍵路徑長度

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

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

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

<!DOCTYPE html>
<html>
<head>
<title>The "Click the button" page</title>
<style>
button {
color: white;
background-color: blue;
}
</style>
</head>
<body>
<button type="button">Click me</button>
<script>
var button = document.querySelector("button");
button.addEventListener("click", function () {
alert("Well done.");
});
</script>
</body>
</html>

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

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

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

關鍵位元組數

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

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

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

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

還要考慮一個事實,就是外部資源是可以被快取的,因此在回訪頁面,或者訪問用相同資源(比如 my-global.css

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

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

它挑出我們的 styles.css

現在我知道一個按鈕就有很多 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 的搜尋引擎更高興。

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

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

在本部落格的下一部分中,我希望給出一個真實世界的例子,說明如何在我自己的團隊的大量程式碼庫中實現所有這些原則。我們在一個域上擁有超過 2000 個可管理內容的頁面,並且每天為數十萬個頁面瀏覽量提供服務,因此這將很有趣。

不過這可能還需要段時間,我現在需要休息一下。

相關文章

前端開發 最新文章