React 官網(wǎng)為什么那么快?
當(dāng)我們打開 React 官網(wǎng)時,會發(fā)現(xiàn)從瀏覽器上輸入url 到頁面首屏完全展示這一過程所花的時間極短,而且在頁面中點擊鏈接切換路由的操作非常順滑,幾乎頁面可以達到“秒切”的效果,根本不會有卡頓等待的情況發(fā)生,于是帶著“react官網(wǎng)到底是怎么做的”疑問開始了本次探索,發(fā)現(xiàn)其主要用了以下的優(yōu)化手段
靜態(tài)站點生成 SSG
下面是react官方中文文檔首頁的截圖,大家注意下方的紅色區(qū)域,后面會作為推斷的一個理由
當(dāng)我們打開控制臺之后,點擊network并選擇 DOC文檔請求,就會發(fā)現(xiàn)有一個請求路徑為https://zh-hans.reactjs.org/ 的GET請求,響應(yīng)結(jié)果為一個 html文檔,里面剛好能找到對應(yīng)上圖中紅色區(qū)域文字的文本,這也就佐證了這個html文檔所對應(yīng)的頁面就是react官網(wǎng)首頁,而這種渲染頁面的方式只有兩種,一種是服務(wù)端渲染 SSR,還有一種是靜態(tài)站點生成 SSG
很多人總是分不清客戶端渲染CSR、服務(wù)端渲染SSR還有靜態(tài)站點生成SSG,下面我們簡單介紹一下它們各自的特點,看完之后相信你就能清晰的感受到它們的區(qū)別所在了
頁面的渲染流程
在開始之前,我們先來回顧一下頁面最基本的渲染流程是怎么樣的?
瀏覽器通過請求得到一個 HTML文本
渲染進程解析 HTML 文本,構(gòu)建 DOM 樹
瀏覽器解析 HTML 的同時,如果遇到內(nèi)聯(lián)樣式或者樣本樣式,則下載并構(gòu)建樣式規(guī)則(stytle rules)。若遇到 Javascript 腳本,則會下載并執(zhí)行腳本
DOM 樹和樣式規(guī)則構(gòu)建完成之后,渲染進程將兩者合并成渲染樹(render tree)
渲染進程開始對渲染樹進行布局,生成布局樹(layout tree)
渲染進程對布局樹進行繪制,生成繪制記錄
渲染進程對布局樹進行分層,分別柵格化每一層并得到合成幀
渲染進程將合成幀發(fā)送給 GPU 進程將圖像繪制到頁面中
可以看到,頁面的渲染其實就是瀏覽器將HTML文本轉(zhuǎn)化為頁面幀的過程,下面我們再來看看剛剛提到的技術(shù):
客戶端渲染 CSR
如今我們大部分 WEB 應(yīng)用都是使用 JavaScript 框架(Vue、React、Angular)進行頁面渲染的,頁面中的大部分DOM元素都是通過Javascript插入的。也就是說,在執(zhí)行 JavaScript 腳本之前,HTML 頁面已經(jīng)開始解析并且構(gòu)建 DOM 樹了,JavaScript 腳本只是動態(tài)的改變 DOM 樹的結(jié)構(gòu),使得頁面成為希望成為的樣子,這種渲染方式叫動態(tài)渲染,也就是平時我們所稱的客戶端渲染 CSR(client side render)
下面代碼為瀏覽器請求 react 編寫的單頁面應(yīng)用網(wǎng)頁時響應(yīng)回的HTML文檔,其實它只是一個空殼,里面并沒有具體的文本內(nèi)容,需要執(zhí)行 JavaScript 腳本之后才會渲染我們真正想要的頁面
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site created using create-react-app" />
<link rel="apple-touch-icon" href="/logo192.png" />
<link rel="manifest" href="/manifest.json" />
<title>Jira任務(wù)管理系統(tǒng)</title>
<script
type="text/javascript">!function (n) { if ("/" === n.search[1]) { var a = n.search.slice(1).split("&").map((function (n) { return n.replace(/~and~/g, "&") })).join("?"); window.history.replaceState(null, null, n.pathname.slice(0, -1) + a + n.hash) } }(window.location)</script>
<link href="/static/css/2.4ddacf8e.chunk.css" rel="stylesheet">
<link href="/static/css/main.cecc54dc.chunk.css" rel="stylesheet">
</head>
<body><noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script>!function (e) { function r(r) { for (var n, a, i = r[0], c = r[1], f = r[2], s = 0, p = []; s < i.length; s++)a = i[s], Object.prototype.hasOwnProperty.call(o, a) && o[a] && p.push(o[a][0]), o[a] = 0; for (n in c) Object.prototype.hasOwnProperty.call(c, n) && (e[n] = c[n]); for (l && l(r); p.length;)p.shift()(); return u.push.apply(u, f || []), t() } function t() { for (var e, r = 0; r < u.length; r++) { for (var t = u[r], n = !0, i = 1; i < t.length; i++) { var c = t[i]; 0 !== o[c] && (n = !1) } n && (u.splice(r--, 1), e = a(a.s = t[0])) } return e } var n = {}, o = { 1: 0 }, u = []; function a(r) { if (n[r]) return n[r].exports; var t = n[r] = { i: r, l: !1, exports: {} }; return e[r].call(t.exports, t, t.exports, a), t.l = !0, t.exports } a.e = function (e) { var r = [], t = o[e]; if (0 !== t) if (t) r.push(t[2]); else { var n = new Promise((function (r, n) { t = o[e] = [r, n] })); r.push(t[2] = n); var u, i = document.createElement("script"); i.charset = "utf-8", i.timeout = 120, a.nc && i.setAttribute("nonce", a.nc), i.src = function (e) { return a.p + "static/js/" + ({}[e] || e) + "." + { 3: "20af26c9", 4: "b947f395", 5: "ced9b269", 6: "5785ecf8" }[e] + ".chunk.js" }(e); var c = new Error; u = function (r) { i.onerror = i.onload = null, clearTimeout(f); var t = o[e]; if (0 !== t) { if (t) { var n = r && ("load" === r.type ? "missing" : r.type), u = r && r.target && r.target.src; c.message = "Loading chunk " + e + " failed.\n(" + n + ": " + u + ")", c.name = "ChunkLoadError", c.type = n, c.request = u, t[1](c) } o[e] = void 0 } }; var f = setTimeout((function () { u({ type: "timeout", target: i }) }), 12e4); i.onerror = i.onload = u, document.head.appendChild(i) } return Promise.all(r) }, a.m = e, a.c = n, a.d = function (e, r, t) { a.o(e, r) || Object.defineProperty(e, r, { enumerable: !0, get: t }) }, a.r = function (e) { "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(e, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(e, "__esModule", { value: !0 }) }, a.t = function (e, r) { if (1 & r && (e = a(e)), 8 & r) return e; if (4 & r && "object" == typeof e && e && e.__esModule) return e; var t = Object.create(null); if (a.r(t), Object.defineProperty(t, "default", { enumerable: !0, value: e }), 2 & r && "string" != typeof e) for (var n in e) a.d(t, n, function (r) { return e[r] }.bind(null, n)); return t }, a.n = function (e) { var r = e && e.__esModule ? function () { return e.default } : function () { return e }; return a.d(r, "a", r), r }, a.o = function (e, r) { return Object.prototype.hasOwnProperty.call(e, r) }, a.p = "/", a.oe = function (e) { throw console.error(e), e }; var i = this.webpackJsonpjira = this.webpackJsonpjira || [], c = i.push.bind(i); i.push = r, i = i.slice(); for (var f = 0; f < i.length; f++)r(i[f]); var l = c; t() }([])</script>
<script src="/static/js/2.2b45c055.chunk.js"></script>
<script src="/static/js/main.3224dcfd.chunk.js"></script>
</body>
</html>
復(fù)制代碼
服務(wù)端渲染 SSR
顧名思義,服務(wù)端渲染就是在瀏覽器請求頁面 URL 的時候,服務(wù)端將我們需要的 HTML 文本組裝好,并返回給瀏覽器,這個 HTML 文本被瀏覽器解析之后,不需要經(jīng)過 JavaScript 腳本的下載過程,即可直接構(gòu)建出我們所希望的 DOM 樹并展示到頁面中。這個服務(wù)端組裝HTML的過程就叫做服務(wù)端渲染 SSR
下面是服務(wù)端渲染時返回的 HTML 文檔,由于代碼量實在是太多,所以只保留了具有象征意義的部分代碼,但不難發(fā)現(xiàn),服務(wù)端渲染返回的HTML文檔中具有頁面的核心文本
<!DOCTYPE html>
<html lang="zh-hans">
<head>
<link rel="preload" as="script" />
<meta name="generator" content="Gatsby 2.24.63" />
<style data-href="/styles.dc271aeba0722d3e3461.css">
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%
}
/* ....many CSS style */
</style>
</head>
<body>
<script>
(function () {
/*
BE CAREFUL!
This code is not compiled by our transforms
so it needs to stay compatible with older browsers.
*/
var activeSurveyBanner = null;
var socialBanner = null;
var snoozeStartDate = null;
var today = new Date();
function addTimes(date, days) {
var time = new Date(date);
time.setDate(time.getDate() + days);
return time;
}
// ...many js code
})();
</script>
<div id="___gatsby">
<!-- ...many html dom -->
<div class="css-1vcfx3l">
<h3 class="css-1qu2cfp">一次學(xué)習(xí),跨平臺編寫</h3>
<div>
<p>無論你現(xiàn)在使用什么技術(shù)棧,在無需重寫現(xiàn)有代碼的前提下,通過引入 React 來開發(fā)新功能。</p>
<p>React 還可以使用 Node 進行服務(wù)器渲染,或使用 <a target="_blank"
rel="nofollow noopener noreferrer">React Native</a> 開發(fā)原生移動應(yīng)用。</p>
</div>
</div>
<!-- ...many html dom -->
</div>
</body>
</html>
復(fù)制代碼
靜態(tài)站點生成 SSG
這也就是React官網(wǎng)所用到的技術(shù),與SSR的相同之處就是對應(yīng)的服務(wù)端同樣是將已經(jīng)組合好的HTML文檔直接返回給客戶端,所以客戶端依舊不需要下載Javascript文件就能渲染出整個頁面,那不同之處又有哪些呢?
使用了SSG技術(shù)搭建出的網(wǎng)站,每個頁面對應(yīng)的HTML文檔在項目build打包構(gòu)建時就已經(jīng)生成好了,用戶請求的時候服務(wù)端不需要再發(fā)送其它請求和進行二次組裝,直接將該HTML文檔響應(yīng)給客戶端即可,客戶端與服務(wù)端之間的通信也就變得更加簡單
但讀到這里很容易會發(fā)現(xiàn)它有幾個致命的弱點:
HTML文檔既然是在項目打包時就已經(jīng)生成好了,那么所有用戶看到的都只能是同一個頁面,就像是一個靜態(tài)網(wǎng)站一樣,這也是這項技術(shù)的關(guān)鍵字眼——靜態(tài)
每次更改內(nèi)容時都需要構(gòu)建和部署應(yīng)用程序,所以其具有很強的局限性,不適合制作內(nèi)容經(jīng)常會變換的網(wǎng)站
但每項技術(shù)的出現(xiàn)都有其對應(yīng)的使用場景,我們不能因為某項技術(shù)的某個缺點就否定它,也不能因為某項技術(shù)的某個優(yōu)點就濫用它! 該技術(shù)還是有部分應(yīng)用場景的,如果您想要搭建一個充滿靜態(tài)內(nèi)容的網(wǎng)站,比如個人博客、項目使用文檔等Web應(yīng)用程序,使用SSG再適合不過了,使用過后我相信你一定能感受到這項技術(shù)的強大之處!
問題解答
現(xiàn)在我們就可以回答為什么react官網(wǎng)要使用SSG這項技術(shù)去做了?
因為相對于客戶端渲染,服務(wù)端渲染和靜態(tài)網(wǎng)點生成在瀏覽器請求URL之后得到的是一個帶有數(shù)據(jù)的HTML文本,并不是一個HTML空殼。瀏覽器只需要解析HTML,直接構(gòu)建DOM樹就可以了。而客戶端渲染,需要先得到一個空的HTML頁面,這個時候頁面已經(jīng)進入白屏,之后還需要經(jīng)過加載并執(zhí)行 JavaScript、請求后端服務(wù)器獲取數(shù)據(jù)、JavaScript 渲染頁面幾個過程才可以看到最后的頁面。特別是在復(fù)雜應(yīng)用中,由于需要加載 JavaScript 腳本,越是復(fù)雜的應(yīng)用,需要加載的 JavaScript腳本就越多、越大,這會導(dǎo)致應(yīng)用的首屏加載時間非常長,從而降低了體驗感
至于SSR與SSG的選取,我們要從應(yīng)用場景出發(fā),到底是用戶每次請求都在服務(wù)端重新組裝一個HTML文檔?還是在項目構(gòu)建的時候就生成一個唯一的HTML文檔呢?
React團隊成員在開發(fā)官網(wǎng)的時候肯定早就想到了這個問題,既然是官網(wǎng),那肯定沒有權(quán)限之分,所有進入到該網(wǎng)站的人看到的內(nèi)容應(yīng)該是一樣的才對,那每次請求都在服務(wù)端組裝一個一模一樣的HTML有什么意義呢?為什么不提前在服務(wù)端渲染好,然后發(fā)給每個人,這樣N次渲染就變成了1次渲染,大大減少了客戶端與服務(wù)端通信的時間,進而提升了用戶體驗
總結(jié)
無論是哪種渲染方式,一開始都是要請求一個 HTML 文本,但是區(qū)別就在于這個文本是否已經(jīng)被服務(wù)端組裝好了
客戶端渲染還需要去下載和執(zhí)行額外的Javascript腳本之后才能得到我們想要的頁面效果,所以速度會比服務(wù)端渲染慢很多
服務(wù)端渲染得到的HTML文檔就已經(jīng)組合好了對應(yīng)的文本,瀏覽器請求到之后直接解析渲染出來即可,不需要再去下載和執(zhí)行額外的Javasript 腳本,所以速度會比客戶端渲染快很多
對于一些內(nèi)容不經(jīng)常變化的網(wǎng)站,我們甚至可以在服務(wù)端渲染的基礎(chǔ)上予以改進,將每次請求服務(wù)端都渲染一次HTML文檔改成總共就只渲染一次,這就是靜態(tài)站點生成技術(shù)
下圖是客戶端渲染和服務(wù)端渲染的流程圖:
一些預(yù)加載/預(yù)處理資源的方式
研究完首屏渲染之后,我們再來研究一下路由跳轉(zhuǎn)后內(nèi)容的切換。經(jīng)???react 文檔的朋友可能早就發(fā)現(xiàn)了,其路由跳轉(zhuǎn)無比絲滑,感覺就像是一個靜態(tài)頁面一樣,完全沒有發(fā)送網(wǎng)絡(luò)請求的痕跡,比如我現(xiàn)在處在hook 簡介這一個板塊,當(dāng)我點擊 hook 規(guī)則 目錄之后
發(fā)現(xiàn)頁面瞬間秒切了過去,內(nèi)容也瞬間展現(xiàn)在了出來,沒有一絲卡頓,用戶體驗直接爆炸,這到底是怎么做到的呢?
下面我們就來一點一點分析它的每個優(yōu)化手段
preload
在當(dāng)前頁面中,你可以指定可能或很快就需要的資源在其頁面生命周期的早期——瀏覽器的主渲染機制介入前就進行預(yù)加載,這可以讓對應(yīng)的資源更早的得到加載并使用,也更不易阻塞頁面的初步渲染,進而提升性能
關(guān)鍵字 preload 作為元素 <link> 的屬性 rel的值,表示用戶十分有可能需要在當(dāng)前瀏覽中加載目標(biāo)資源,所以瀏覽器必須預(yù)先獲取和緩存對應(yīng)資源 。下面我們來看一個示例:
<link as="script" rel="preload" href="/webpack-runtime-732352b70a6d0733ac95.js">
復(fù)制代碼
這樣做的好處就是讓在當(dāng)前頁面中可能被訪問到的資源提前加載但并不阻塞頁面的初步渲染,進而提升性能
下面是 react文檔中對 preload關(guān)鍵字的使用,告訴瀏覽器等等可能需要這個資源,希望能夠盡早下載下來
可以預(yù)加載的資源有很多,現(xiàn)在瀏覽器支持的主要有:
audio:音頻文件,通常用于 audio 標(biāo)簽
document: 旨在由 frame 或嵌入的 HTML 文檔
embed:要嵌入到 embed 元素中的資源
fetch:要通過 fetch 或 XHR 請求訪問的資源,例如 ArrayBuffer 或 JSON 文件
font: 字體文件
image:圖像文件
object:要嵌入到 object 元素中的資源
script: JavaScript 文件
style: CSS 樣式表
track: WebVTT 文件
worker:一個 JavaScript 網(wǎng)絡(luò)工作者或共享工作者
video:視頻文件,通常用于 video 標(biāo)簽
注意:使用 preload作為 link標(biāo)簽rel屬性的屬性值的話一定要記得在標(biāo)簽上添加 as屬性,其屬性值就是要預(yù)加載的內(nèi)容類型
preconnect
元素屬性的關(guān)鍵字preconnect是提示瀏覽器用戶可能需要來自目標(biāo)域名的資源,因此瀏覽器可以通過搶先啟動與該域名的連接來改善用戶體驗 —— MDN
下面來看一個用法示例:
<link rel="preconnect" >
復(fù)制代碼
下面是 react官方文檔中的使用:
簡單來說就是提前告訴瀏覽器,在后面的js代碼中可能會去請求這個域名下對應(yīng)的資源,你可以先去把網(wǎng)絡(luò)連接建立好,到時候發(fā)送對應(yīng)請求時也就更加快速
dns-prefetch
DNS-prefetch (DNS 預(yù)獲取) 是嘗試在請求資源之前解析域名。這可能是后面要加載的文件,也可能是用戶嘗試打開的鏈接目標(biāo) —— MDN
那我們?yōu)槭裁匆M行域名預(yù)解析呢?這里面其實涉及了一些網(wǎng)絡(luò)請求的東西,下面簡單介紹一下:
當(dāng)瀏覽器從(第三方)服務(wù)器請求資源時,必須先將該跨域域名解析為 IP 地址,然后瀏覽器才能發(fā)出請求。此過程稱為 DNS 解析。DNS 緩存可以幫助減少此延遲,而 DNS 解析可以導(dǎo)致請求增加明顯的延遲。對于打開了與許多第三方的連接的網(wǎng)站,此延遲可能會大大降低加載性能。預(yù)解析域名就是為了在真正發(fā)請求的時候減少延遲,從而在一定程度上提高性能
用法示例:
<link rel="dns-prefetch" >
復(fù)制代碼
下面是 react官方文檔中的使用:
通俗點來說,dns-prefetch 的作用就是告訴瀏覽器在給第三方服務(wù)器發(fā)送請求之前去把指定域名的解析工作給做了,這個優(yōu)化方法一般會和上面的preconnect一起使用,這些都是性能優(yōu)化的一些手段,我們也可以在自己項目中合適的地方來使用
prefetch
關(guān)鍵字 prefetch 作為元素 的屬性 rel 的值,是為了提示瀏覽器,用戶未來的瀏覽有可能需要加載目標(biāo)資源,所以瀏覽器會事先獲取和緩存對應(yīng)資源,優(yōu)化用戶體驗 ——MDN
上面的解釋已經(jīng)很通俗易懂了,就是告訴瀏覽器用戶未來可能需要這些資源,這樣瀏覽器可以提前獲取這些資源,等到用戶真正需要使用這些資源的時候一般都已經(jīng)加載好了,內(nèi)容展示就會十分的流暢
用法示例:
<link rel="prefetch" href="/page-data/docs/getting-started.html/page-data.json" crossorigin="anonymous" as="fetch">
復(fù)制代碼
可以看到 react文檔在項目中大量使用到了 prefetch來優(yōu)化項目
那么我們在什么情況下使用 prefetch才比較合適呢?
像 react文檔一樣,當(dāng)你的頁面中具有可能跳轉(zhuǎn)到其他頁面的路由鏈接時,就可以使用prefetch 預(yù)請求對應(yīng)頁面的資源了
但如果一個頁面中這樣的路由鏈接很多呢?那豈不是要大量的發(fā)送網(wǎng)絡(luò)請求,雖然現(xiàn)在流量很便宜,但你也不能那么玩?。。╠oge)
React 當(dāng)然考慮到了這個問題,因為在它的文檔中包含有大量的路由鏈接,不可能全部都發(fā)一遍請求,這樣反而不利于性能優(yōu)化,那react是怎么做的呢?
通過監(jiān)聽 Link元素,當(dāng)其出現(xiàn)到可見區(qū)域時動態(tài)插入帶有prefetch屬性值的link標(biāo)簽到HTML文檔中,從而去預(yù)加載對應(yīng)路由頁面的一些資源,這樣當(dāng)用戶點擊路由鏈接跳轉(zhuǎn)過去時由于資源已經(jīng)請求好所以頁面加載會特別快
舉個例子,還沒有點擊下圖中劃紅線的目錄時,由于其子目錄沒有暴露到視圖窗口中,所以頁面中并沒有對應(yīng)的標(biāo)簽,而當(dāng)點擊了該目錄后,其子目錄就會展示在視圖窗口中,react會自動將暴露出來的路由所對應(yīng)的數(shù)據(jù)通過prefetch提前請求過來,這樣當(dāng)用戶點擊某個子目錄的時候,由于已經(jīng)有了對應(yīng)的數(shù)據(jù),直接獲取內(nèi)容進行展示即可。用這樣的方法,我們感受到的速度能不快嗎?
下面是我們在network查看到的結(jié)果
補充
react官網(wǎng)其實并不完全是由react這個框架進行開發(fā)的,能做上述所說的那么多性能優(yōu)化其實得益于Gatsby這個庫
Gatsby 是一個性能很好,開發(fā)很自由的,基于 React 和 GraphQL 來構(gòu)建網(wǎng)站的庫。一般用于構(gòu)建靜態(tài)網(wǎng)站,比如博客、企業(yè)官網(wǎng)等,或者說靜態(tài)內(nèi)容相對比較多的網(wǎng)站
它在打包的時候就生成了所有頁面對應(yīng)的 HTML文件以及數(shù)據(jù)文件等,這樣當(dāng)你訪問某個頁面時,服務(wù)端可以直接返回HTML ,另外一方面當(dāng)頁面中有使用 Link 時,會提前加載這個頁面所對應(yīng)的數(shù)據(jù),這樣點擊跳轉(zhuǎn)后頁面加載速度就會很快。所以上文中所說的優(yōu)化手段,其實是 Gatsby幫助實現(xiàn)的,有興趣的朋友可以去它的官網(wǎng)了解更多相關(guān)知識
至于這個監(jiān)聽Link元素是怎么實現(xiàn)的呢?
具體實現(xiàn)是使用 Intersection Observer ,相關(guān)介紹見 IntersectionObserver API 使用教程 - 阮一峰的網(wǎng)絡(luò)日志 ,有寫到圖片懶加載和無限滾動也可以使用這個 API 去實現(xiàn),只不過現(xiàn)在有個別瀏覽器還沒有支持,所以在兼容性上存在一些阻攔,導(dǎo)致這個 Api現(xiàn)在還沒有被普及
參考
本篇文章參考了以下幾篇文章并結(jié)合上了自己的理解,下面文章個人覺得質(zhì)量真的很高,大家也可以去看看。另外大家在文章中如果發(fā)現(xiàn)問題可以在評論區(qū)中指出,大家共同進步~
github.com/findxc/blog…
github.com/findxc/blog…
作者:Running53
鏈接:https://juejin.cn/post/7128369638794231839
作者:Running53
歡迎關(guān)注微信公眾號 :深圳灣碼農(nóng)