服务器端渲染 (SSR) 是一种非常有用的技术,可以使 Web 应用程序看起来更快。在 JavaScript 解析之前显示初始 HTML,并且当用户决定点击什么时,我们的处理程序已经准备就绪。
React 中的服务器端渲染需要额外的设置工作,并产生服务器成本。此外,如果您的服务器团队无法在您的服务器上运行 JavaScript,您就会陷入困境。它会极大地复杂化 CDN 设置,尤其是当您有需要登录的页面以及用户的信息需要管理时。
我想介绍一个名为渲染缓存的新概念。这是一个很酷的技巧,它可以为用户提供与 SSR 相似的即时性能提升,而无需诉诸在服务器上编写代码。
什么是渲染缓存?
从静态 HTML 页面迁移到单页面应用程序 (SPA) 使 Web 传统上依赖的整个缓存概念出现了巨大漏洞。虽然浏览器优化了初始 HTML 的传递和渲染,但 SPA 会将它们留空,以便稍后填充。
渲染缓存优化 SPA 渲染,可以显着提高 Web 页面的感知加载时间。它通过在浏览器中缓存渲染的 HTML 来实现这一点,以便在下次加载时提供该显示,而无需执行会占用显示时间的 JavaScript 解析。
启用渲染缓存
我们之前提到,为 React 设置 SSR 需要额外的设置和服务器成本。渲染缓存避免了这些负担。
设置它需要几个步骤。让我们将其分解成易于理解的部分。
步骤 1:确定正确的缓存状态
确定当前页面在用户下次访问时渲染相同的条件。
例如,您可以创建一个包含当前构建编号或用户 ID 的 JSON 对象。关键是确保状态封装在 URL、本地存储或 Cookie 中,并且不需要服务器调用。
步骤 2:设置 API 调用
确保所有 API 调用都发生在渲染 React 的调用之前。这在常规用例中也是有意义的,因为我们希望防止页面在用户下方发生更改,从而导致闪烁。
步骤 3:在卸载处理程序中本地缓存
现在向文档添加卸载事件处理程序。将当前 DOM 存储在 localStorage
/indexDB
中。
这看起来像这样,使用构建编号和用户 ID 来确定步骤 1 中介绍的缓存状态
window.addEventListener("beforeunload", () => {
// Production code would also be considerate of localStorage size limitations
// and would do a LRU cache eviction to maintain sanity on storage.
// There should also be a way to clear this data when the user signs out
window.localStorage.setItem(
`lastKnown_${window.location.href}`,
JSON.stringify({
conditions: {
userId: "<User ID>",
buildNo: "<Build No.>"
},
data: document.getElementById("content").innerHTML
})
);
});
// If you want to store data per user, you can add user ID to the key instead of the condition.
步骤 4:在加载时恢复最后已知的状态
接下来,我们希望从浏览器的本地存储中提取最后已知的状态,以便我们可以在以后的访问中使用它。我们通过在 HTML 文件(例如在文档的 the body 标记下的 index.html
)中添加以下内容来实现此目的。
<!-- ... -->
</body>
<script>
let lastKnownState = window.localStorage.getItem(`lastKnown_${window.location.href}`);
lastKnownState = lastKnownState && JSON.parse(lastKnownState);
if (lastKnownState &&
lastKnownState.conditions.userId === "<User ID>" &&
lastKnownState.conditions.buildNo === "<Build No.>") {
document.getElementById('content').innerHTML = lastKnownState.data;
window.hasRestoredState = true;
}
</script>
步骤 5:在 React 中渲染最后已知的状态
这是重点所在。现在,我们已经将用户的最后已知状态在 DOM 中可见,我们可以通过使用 hydrate 有条件地更新 React 渲染的顶层来获取完整内容并以该状态渲染我们的应用程序。事件处理程序将在此代码命中后变为功能性,但 DOM 不应更改。
import {render, hydrate} from "react-dom"
if (window.hasRestoredState) {
hydrate(<MyPage />, document.getElementById('content'));
} else {
render(<MyPage />, document.getElementById('content'));
}
步骤 6:始终异步
将您的脚本标记从 sync
更改为 async
/defer
以加载 JavaScript 文件。这是确保前端平滑加载和渲染体验的另一个关键步骤。
就是这样!重新加载页面以查看性能提升。
衡量改进
好的,您做了所有这些工作,现在您想知道您的网站的性能如何。您需要对改进进行基准测试。
渲染缓存在您在知道要渲染的内容之前需要进行多次服务器调用时表现出色。在脚本密集型页面上,JavaScript 实际上可能需要很长时间才能解析。
您可以在 Chrome 的 DevTools 中的性能选项卡中衡量加载性能。

理想情况下,您将使用来宾配置文件,以便您的浏览器扩展不会干扰测量。您应该在重新加载时看到显着的改进。在上面的屏幕截图中,我们有一个示例应用程序,其中包含一个异步 data.json
获取调用,该调用在调用 ReactDOM.hydrate
之前执行。使用渲染缓存,渲染甚至在数据加载之前就完成了!
总结
渲染缓存是一种巧妙的技术,通过向最终 HTML 添加缓存层并将其显示给用户,以确保相同 Web 页面的重新获取的感知速度更快。经常访问您网站的用户将是受益最大的用户。
如您所见,我们使用很少的代码就实现了这一点,而我们获得的性能提升是巨大的。请在您的网站上尝试一下,并发表您的评论。我很想知道您的网站性能是否也获得了与我所体验到的相同的显着提升。
“确保所有 API 调用都发生在渲染 React 的调用之前。”
这是一项非常棘手的任务,因为这意味着不能采用在 componentDidMount 中对每个组件进行 API 调用的 React 常规做法。您有关于如何执行此操作的示例吗?
嗨,匿名用户,
如果你等到 componentDidMount 之后才进行服务器调用,那么服务器调用就必须等到组件完成一次渲染才能执行,即使没有涉及渲染缓存,也会造成至少一次闪烁。使用渲染缓存时,第一次渲染会删除 DOM 上的渲染数据。如果两个组件需要相同的数据,则需要进行两次 API 调用。
我会将渲染和数据部分分开,并使用状态管理库在两者之间传递信息。因此,服务器访问可以独立于渲染并更接近于路由。这种方法也适用于服务器端渲染,原因相同。
在 componentDidMount 中进行服务器调用是推荐的方式,因为 componentWillMount 可能在组件挂载之前被调用多次,如果你在构造函数或 componentWillMount 中进行调用,调用可能会在组件渲染之前返回,并且在组件渲染之前调用 setState 是非法的。
我并不是在主张使用 componentWillMount。建议将服务器访问移出 React,并将数据存储在状态管理库(如 MobX 或 Redux)中,而不是在需要渲染缓存的情况下使用 React 的状态。然后你可以在挂载 React 之前设置数据。
在React 服务器端渲染揭秘中,Alex Moldovan 展示了一种利用名为 serverFetch 的静态声明结合 Redux Thunks 的模式。我借鉴了这个想法以及其他许多想法,并将其整合到Paragons 中,它提供了开箱即用的 SSR。
David Munger 报告说,React 17 中出现了以下错误:“警告:没有预料到服务器 HTML 包含
<div>
在<div>
中。”