服務端渲染(SSR)近兩年炒得很火熱,相信各位同學對這個名詞多少有所耳聞。本節我們將圍繞“是什么”(服務端渲染的運行機制)、“為什么”(服務端渲染解決了什么性能問題 )、“怎么做”(服務端渲染的應用實例與使用場景)這三個點,對服務端渲染進行探索。
服務端渲染是一個相對的概念,它的對立面是“客戶端渲染”。在運行機制解析這部分,我們會借力客戶端渲染的概念,來幫大家理解服務端渲染的工作方式。基于對工作方式的了解,再去深挖它的原理與優勢。
任何知識點都不是“一座孤島”,服務端渲染的實踐往往與當下流行的前端技術(譬如 Vue,React,Redux 等)緊密結合。本節下半場將以 React 和 Vue 下的服務端渲染實現為例,為大家呈現一個完整的 SSR 實現過程。
## 服務端渲染的運行機制
相對于服務端渲染,同學們普遍對客戶端渲染接受度更高一些,所以我們先從大家喜聞樂見的客戶端渲染說起。
### 客戶端渲染
客戶端渲染模式下,服務端會把渲染需要的靜態文件發送給客戶端,客戶端加載過來之后,自己在瀏覽器里跑一遍 JS,根據 JS 的運行結果,生成相應的 DOM。這種特性使得客戶端渲染的源代碼總是特別簡潔,往往是這個德行:
```
<!doctype html>
<html>
<head>
<title>我是客戶端渲染的頁面</title>
</head>
<body>
<div id='root'></div>
<script src='index.js'></script>
</body>
</html>
```
根節點下到底是什么內容呢?你不知道,我不知道,只有瀏覽器把 index.js 跑過一遍后才知道,這就是典型的客戶端渲染。
**頁面上呈現的內容,你在 html 源文件里里找不到**——這正是它的特點。
### 服務端渲染
服務端渲染的模式下,當用戶第一次請求頁面時,由服務器把需要的組件或頁面渲染成 HTML 字符串,然后把它返回給客戶端。客戶端拿到手的,是可以直接渲染然后呈現給用戶的 HTML 內容,不需要為了生成 DOM 內容自己再去跑一遍 JS 代碼。
使用服務端渲染的網站,可以說是“所見即所得”,**頁面上呈現的內容,我們在 html 源文件里也能找到**。
比如知乎就是典型的服務端渲染案例:

zhihu.com 返回的 HTML 文件已經是可以直接進行渲染的內容了。
## 服務端渲染解決了什么性能問題
事實上,很多網站是出于效益的考慮才啟用服務端渲染,性能倒是在其次。
假設 A 網站頁面中有一個關鍵字叫“前端性能優化”,這個關鍵字是 JS 代碼跑過一遍后添加到 HTML 頁面中的。那么客戶端渲染模式下,我們在搜索引擎搜索這個關鍵字,是找不到 A 網站的——搜索引擎只會查找現成的內容,不會幫你跑 JS 代碼。A 網站的運營方見此情形,感到很頭大:搜索引擎搜不出來,用戶找不到我們,誰還會用我的網站呢?為了把“現成的內容”拿給搜索引擎看,A 網站不得不啟用服務端渲染。
但性能在其次,不代表性能不重要。服務端渲染解決了一個非常關鍵的性能問題——首屏加載速度過慢。在客戶端渲染模式下,我們除了加載 HTML,還要等渲染所需的這部分 JS 加載完,之后還得把這部分 JS 在瀏覽器上再跑一遍。這一切都是發生在用戶點擊了我們的鏈接之后的事情,在這個過程結束之前,用戶始終見不到我們網頁的廬山真面目,也就是說用戶一直在等!相比之下,服務端渲染模式下,服務器給到客戶端的已經是一個直接可以拿來呈現給用戶的網頁,中間環節早在服務端就幫我們做掉了,用戶豈不“美滋滋”?
## 服務端渲染的應用實例
下面我們先來看一下在一個 React 項目里,服務端渲染是怎么實現的。本例中,我們使用 Express 搭建后端服務。
項目中有一個叫做 VDom 的 React 組件,它的內容如下。
VDom.js:
```
import React from 'react'
const VDom = () => {
return <div>我是一個被渲染為真實DOM的虛擬DOM</div>
}
export default VDom
```
在服務端的入口文件中,我引入這個組件,對它進行渲染:
```
import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import VDom from './VDom'
// 創建一個express應用
const app = express()
// renderToString 是把虛擬DOM轉化為真實DOM的關鍵方法
const RDom = renderToString(<VDom />)
// 編寫HTML模板,插入轉化后的真實DOM內容
const Page = `
<html>
<head>
<title>test</title>
</head>
<body>
<span>服務端渲染出了真實DOM: </span>
${RDom}
</body>
</html>
`
// 配置HTML內容對應的路由
app.get('/index', function(req, res) {
res.send(Page)
})
// 配置端口號
const server = app.listen(8000)
```
根據我們的路由配置,當我訪問 [http://localhost:8000/index](http://localhost:8000/index) 時,就可以呈現出服務端渲染的結果了:

我們可以看到,VDom 組件已經被 renderToString 轉化為了一個內容為`<div data-reactroot="">我是一個被渲染為真實DOM的虛擬DOM</div>`的字符串,這個字符串被插入 HTML 代碼,成為了真實 DOM 樹的一部分。
那么 Vue 是如何實現服務端渲染的呢?
其實是一個套路,我這里基于 [Vue SSR 指南](https://ssr.vuejs.org/zh/#%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%AB%AF%E6%B8%B2%E6%9F%93-ssr-%EF%BC%9F) 中官方給出的例子為大家講解 Vue 中的實現思路(思路見注釋)。
該示例直接將 Vue 實例整合進了服務端的入口文件中:
```
const Vue = require('vue')
// 創建一個express應用
const server = require('express')()
// 提取出renderer實例
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => {
// 編寫Vue實例(虛擬DOM節點)
const app = new Vue({
data: {
url: req.url
},
// 編寫模板HTML的內容
template: `<div>訪問的 URL 是: {{ url }}</div>`
})
// renderToString 是把Vue實例轉化為真實DOM的關鍵方法
renderer.renderToString(app, (err, html) => {
if (err) {
res.status(500).end('Internal Server Error')
return
}
// 把渲染出來的真實DOM字符串插入HTML模板中
res.end(`
<!DOCTYPE html>
<html lang="en">
<head><title>Hello</title></head>
<body>${html}</body>
</html>
`)
})
})
server.listen(8080)
```
大家對比一下 React 項目中的注釋內容,是不是發現這兩段代碼從本質上來說區別不大呢?
以上兩個小??,為大家演示了基本的服務端渲染實現流程。
實際項目比這些復雜很多,但萬變不離其宗。強調的只有兩點:一是這個 renderToString() 方法;二是把轉化結果“塞”進模板里的這一步。這兩個操作是服務端渲染的靈魂操作。在虛擬 DOM“橫行”的當下,服務端渲染不再是早年 JSP 里簡單粗暴的字符串拼接過程,它還要求這一端要具備將虛擬 DOM 轉化為真實 DOM 的能力。與其說是“把 JS 在服務器上先跑一遍”,不如說是“把 Vue、React 等框架代碼先在 Node 上跑一遍”。
## 服務端渲染的應用場景
打眼一看,這個服務端渲染給瀏覽器省了這么多事兒,性能肯定是質的飛躍啊!喜聞樂見!但是大家打開自己經常訪問的那些網頁看一看,會發現仍然有許多網站壓根兒不用服務端渲染——看來這個東西也不是萬能的。
根據我們前面的描述,不難看出,服務端渲染本質上是**本該瀏覽器做的事情,分擔給服務器去做**。這樣當資源抵達瀏覽器時,它呈現的速度就快了。乍一看好像很合理:瀏覽器性能畢竟有限,服務器多牛逼!能者多勞,就該讓服務器多干點活!
但仔細想想,在這個網民遍地的時代,幾乎有多少個用戶就有多少臺瀏覽器。用戶擁有的瀏覽器總量多到數不清,那么一個公司的服務器又有多少臺呢?我們把這么多臺瀏覽器的渲染壓力集中起來,分散給相比之下數量并不多的服務器,服務器肯定是承受不住的。
這樣分析下來,服務端渲染也并非萬全之策。在實踐中,我一般會建議大家先忘記服務端渲染這個事情——服務器稀少而寶貴,但首屏渲染體驗和 SEO 的優化方案卻很多——我們最好先把能用的低成本“大招”都用完。除非網頁對性能要求太高了,以至于所有的招式都用完了,性能表現還是不盡人意,這時候我們就可以考慮向老板多申請幾臺服務器,把服務端渲染搞起來了~
- 開篇:知識體系與小冊格局
- 網絡篇 1:webpack 性能調優與 Gzip 原理
- 網絡篇 2:圖片優化——質量與性能的博弈
- 存儲篇 1:瀏覽器緩存機制介紹與緩存策略剖析
- 存儲篇 2:本地存儲——從 Cookie 到 Web Storage、IndexDB
- 彩蛋篇:CDN 的緩存與回源機制解析
- 渲染篇 1:服務端渲染的探索與實踐
- 渲染篇 2:知己知彼——解鎖瀏覽器背后的運行機制
- 渲染篇 3:對癥下藥——DOM 優化原理與基本實踐
- 渲染篇 4:千方百計——Event Loop 與異步更新策略
- 渲染篇 5:最后一擊——回流(Reflow)與重繪(Repaint)
- 應用篇 1:優化首屏體驗——Lazy-Load 初探
- 應用篇 2:事件的節流(throttle)與防抖(debounce)
- 性能監測篇:Performance、LightHouse 與性能 API
- 前方的路:希望成為你的起點