之前只听过 SSR
和 CSR
这两个概念,分别指网页是由服务端渲染还是客户端渲染后由浏览器生成相应页面的技术。最原始的网页一直是客户端通过 JavaScript 请求数据并操作 DOM 元素后生成最终的 DOM 树再由浏览器的渲染进程进行绘制,后来为了优化网页的加载速度,将生成页面元素的过程由服务端进行,从而减轻客户端的工作负荷。
我们先来回顾下网页的渲染详细过程:
- 浏览器通过请求得到一个
HTML
文本 - 渲染进程解析
HTML
文本,构建DOM
树 - 浏览器解析 HTML 的同时,如果遇到内联样式或者样本样式,则下载并构建样式规则(
stytle rules
)。若遇到Javascript
脚本,则会下载并执行脚本 - DOM 树和样式规则构建完成之后,渲染进程将两者合并成渲染树(
render tree
) - 渲染进程开始对渲染树进行布局,生成布局树(
layout tree
) - 渲染进程对布局树进行绘制,生成绘制记录
- 渲染进程对布局树进行分层,分别栅格化每一层并得到合成帧
- 渲染进程将合成帧发送给
GPU
进程将图像绘制到页面中
可以看到,页面的渲染其实就是浏览器将HTML文本转化为页面帧的过程,下面我们再来看看刚刚提到的技术:
客户端渲染 CSR
如今大部分 WEB
应用都是使用 JavaScript
的几个主流框架(Vue
、React
、Angular
)进行页面渲染的,页面中的大部分 DOM
元素都是通过 Javascript
插入的。也就是说,在执行 JavaScript
脚本之前,HTML
页面已经开始解析并且构建 DOM
树了,JavaScript
脚本只是动态的改变 DOM
树的结构,使得页面成为希望成为的样子,这种渲染方式叫动态渲染,也就是平时我们所称的客户端渲染 CSR
(client side render
)
下面代码为浏览器请求 react
编写的单页面应用网页时响应回的HTML文档,其实它只是一个空壳,里面并没有具体的文本内容,需要执行 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>XXX 管理系统</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>
服务端渲染 SSR
顾名思义,服务端渲染就是在浏览器请求页面 URL
的时候,服务端将我们需要的 HTML
文本组装好,并返回给浏览器,这个 HTML
文本被浏览器解析之后,不需要经过 JavaScript
脚本的下载过程,即可直接构建出我们所希望的 DOM
树并展示到页面中。这个服务端组装 HTML
的过程就叫做服务端渲染 SSR
下面是服务端渲染时返回的 HTML
文档,由于代码量实在是太多,所以只保留了具有象征意义的部分代码,但不难发现,服务端渲染返回的 HTML
文档已经是浏览器最终渲染所需要的文本内容
<!DOCTYPE html>
<html lang="zh-hans">
<head>
<link rel="preload" href="https://unpkg.com/docsearch.js@2.4.1/dist/cdn/docsearch.min.js" 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">一次学习,跨平台编写</h3>
<div>
<p>无论你现在使用什么技术栈,在无需重写现有代码的前提下,通过引入 React 来开发新功能。</p>
<p>React 还可以使用 Node 进行服务器渲染,或使用 <a href="https://reactnative.dev/" target="_blank" rel="nofollow noopener noreferrer">React Native</a> 开发原生移动应用。</p>
</div>
</div>
<!-- ...many html dom -->
</div>
</body>
</html>
了解了前两种渲染技术,这里引入我们今天要讲的第三种,也就是静态站点生成 SSG,
静态站点生成 SSG
这也就是 React
官网所用到的技术,与 SSR
的相同之处就是对应的服务端同样是将已经组合好的HTML
文档直接返回给客户端,所以客户端依旧不需要下载 Javascript
文件就能渲染出整个页面,那不同之处又有哪些呢?
使用了 SSG
技术搭建出的网站,每个页面对应的 HTML
文档在项目 build
打包构建时就已经生成好了,用户请求的时候服务端不需要再发送其它请求和进行二次组装,直接将该 HTML
文档响应给客户端即可,客户端与服务端之间的通信也就变得更加简单
但读到这里很容易会发现它有几个致命的弱点:
-
HTML
文档既然是在项目打包时就已经生成好了,那么所有用户看到的都只能是同一个页面,就像是一个静态网站一样,这也是这项技术的关键字眼——静态 - 每次更改内容时都需要构建和部署应用程序,所以其具有很强的局限性,不适合制作内容经常会变换的网站
但每项技术的出现都有其对应的使用场景,我们不能因为某项技术的某个缺点就否定它,也不能因为某项技术的某个优点就滥用它! 该技术还是有部分应用场景的,如果您想要搭建一个充满静态内容的网站,比如个人博客、项目使用文档等 Web
应用程序,使用 SSG
再适合不过了,使用过后我相信你一定能感受到这项技术的强大之处!
总结
无论是哪种渲染方式,一开始都是要请求一个 HTML
文本,但是区别就在于这个文本是否已经被服务端组装好了
客户端渲染还需要去下载和执行额外的 Javascript
脚本之后才能得到我们想要的页面效果,所以速度会比服务端渲染慢很多,而服务端渲染得到的 HTML
文档就已经组合好了对应的文本,浏览器请求到之后直接解析渲染出来即可,所以速度会比客户端渲染快很多。
对于一些内容不经常变化的网站,我们甚至可以在服务端渲染的基础上予以改进,将每次请求服务端都渲染一次 HTML
文档改成总共就只渲染一次,这就是静态站点生成技术。