惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

J
Java Code Geeks
aimingoo的专栏
aimingoo的专栏
Martin Fowler
Martin Fowler
C
Check Point Blog
G
Google Developers Blog
V
Visual Studio Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
有赞技术团队
有赞技术团队
MongoDB | Blog
MongoDB | Blog
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
大猫的无限游戏
大猫的无限游戏
D
Docker
Hugging Face - Blog
Hugging Face - Blog
The GitHub Blog
The GitHub Blog
博客园 - 三生石上(FineUI控件)
A
About on SuperTechFans
Recent Announcements
Recent Announcements
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
阮一峰的网络日志
阮一峰的网络日志
Stack Overflow Blog
Stack Overflow Blog
Vercel News
Vercel News

博客园 - 梦烬

收到一个网页链接,先看清再决定要不要打开:MeowToolkit 的链接提取 不改 iPhone 系统语言,也能单独切换扫描工具:MeowScanner 的中英界面入口 待办写下后还是会忘:妙啊清单把截止事项带进时间线 想养成习惯却总忘记开始:Steady 把它变成今天的一段计时 重要日期散在备忘录里:妙啊时刻把“还有多久”放回时间线 Uber 与 Anthropic 关于 AI Skills 的实践与洞察 Cursor公布的这张图,价值千金 我用AI做了第一个付费App,现已上架AppStore 我用AI做了第一个付费App,现已上架AppStore 图解React SSR中的hydrate React SSR源码剖析 SSR 与当年的 JSP、PHP 有什么区别? 2020 SSR落地开花的三大机遇 SSR 它到底香不香?细数 SSR 的利与弊 假如 Web 当初不支持动态化 如何提升前端基建的效能价值? 如何衡量前端基建的效能价值? 从面向对象角度看前端工程体系 「前端工程化」该怎么理解? 跨端方案的三大困境 React 17 要来了,非常特别的一版 low-code 与 20 年前的 Dreamweaver 有什么区别? 什么才是定制化IDE的核心价值? 移动端跨平台技术之下的变与不变
精通 React SSR 之 API 篇
梦烬 · 2020-11-19 · via 博客园 - 梦烬

React 提供的 SSR API 分为两部分,一部分面向服务端(react-dom/server),另一部分仍在客户端执行(react-dom

react ssr

ReactDOMServer相关 API 能够在服务端将 React 组件渲染成静态的(HTML)标签

The ReactDOMServer object enables you to render components to static markup.

把组件树渲染成对应 HTML 标签的工作在浏览器环境也能完成,因此,面向服务端的 React DOM API 也分为两类:

ReactDOMServer.renderToString(element)

最基础的 SSR API,输入 React 组件(准确来说是ReactElement),输出 HTML 字符串。之后由客户端 hydrate API 对服务端返回的视图结构附加上交互行为,完成页面渲染:

If you call ReactDOM.hydrate() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers.

ReactDOMServer.renderToStaticMarkup(element)

renderToString类似,区别在于 API 设计上,renderToStaticMarkup只用于纯展示(没有事件交互,不需要 hydrate)的场景

This is useful if you want to use React as a simple static page generator, as stripping away the extra attributes can save some bytes. If you plan to use React on the client to make the markup interactive, do not use this method. Instead, use renderToString on the server and ReactDOM.hydrate() on the client.

因此renderToStaticMarkup只生成干净的 HTML,不带额外的 DOM 属性(如data-reactroot),响应体积上有些微的优势

之所以说体积优势些微,是因为在 React 16 之前,SSR 采用的是基于字符串校验和(string checksum)的 HTML 节点复用方式,字对字地严格校验一致性,一旦发现不匹配就完全丢弃服务端渲染结果,在客户端重新渲染:

If for any reason there’s a mismatch, React raises a warning in development mode and replaces the entire tree of server-generated markup with HTML that has been generated on the client.

生成了大量的额外属性:

// renderToString
<div data-reactroot="" data-reactid="1"
    data-react-checksum="122239856">
  <!-- react-text: 2 -->This is some <!-- /react-text -->
  <span data-reactid="3">server-generated</span>
  <!-- react-text: 4--> <!-- /react-text -->
  <span data-reactid="5">HTML.</span>
</div>

这时候renderToStaticMarkup生成干净清爽的 HTML 还有着不小的体积优势:

// renderToStaticMarkup
<div data-reactroot="">
  This is some <span>server-generated</span> <span>HTML.</span>
</div>

React 16 改用单节点校验来复用(服务端返回的)HTML 节点,不再生成data-reactiddata-react-checksum等体积占用大户,两个 API 渲染结果的体积差异变得微乎其微。例如,对于 React 组件:

class MyComponent extends React.Component {
  state = {
      title: 'Welcome to React SSR!',
    };
  }

  render() {
    return (
      <div>
        <h1 className="here">
          {this.state.title} Hello There!
        </h1>
      </div>
    );
  }
}

二者的渲染结果分别为:

// renderToString
<div data-reactroot=""><h1 class="here">Welcome to React SSR!<!-- --> Hello There!</h1></div>

// renderToStaticMarkup
<div><h1 class="here">Welcome to React SSR! Hello There!</h1></div>

也就是说,目前(2020/11/8,React 17.0.1)renderToStaticMarkuprenderToString的实际差异主要在于:

  • renderToStaticMarkup不生成data-reactroot

  • renderToStaticMarkup不在相邻文本节点之间生成<!-- -->(相当于合并了文本节点,不考虑节点复用,算是针对静态渲染的额外优化措施)

ReactDOMServer.renderToNodeStream(element)

对应于renderToString的 Stream API,将renderToString生成的 HTML 字符串以Node.js Readable stream形式返回

P.S.默认返回utf-8 编码的字节流,其它编码格式需自行转换

P.S.该 API 的实现依赖Node.js 的 Stream 特性,所以不能在浏览器环境使用

ReactDOMServer.renderToStaticNodeStream(element)

对应于renderToStaticMarkup的 Stream API,将renderToStaticMarkup生成的干净 HTML 字符串以Node.js Readable stream形式返回

P.S.同样按 utf-8 编码,并且不能在浏览器环境使用

ReactDOM.hydrate(element, container[, callback])

与常用的render()函数签名完全一致:

ReactDOM.render(element, container[, callback])

hydrate()配合 SSR 使用,与render()的区别在于渲染过程中能够复用服务端返回的现有 HTML 节点,只为其附加交互行为(事件监听等),并不重新创建 DOM 节点:

React will attempt to attach event listeners to the existing markup.

需要注意的是,服务端返回的 HTML 与客户端渲染结果不一致时,出于性能考虑,hydrate()并不纠正除文本节点外的 SSR 渲染结果,而是将错就错

There are no guarantees that attribute differences will be patched up in case of mismatches. This is important for performance reasons because in most apps, mismatches are rare, and so validating all markup would be prohibitively expensive.

只在development模式下对这些不一致的问题报 Warning,因此必须重视 SSR HydrationWarning,要当 Error 逐个解决:

This performance optimization means that you will need to make extra sure that you fix any markup mismatch warnings you see in your app in development mode.

特殊的,对于意料之中的不一致问题,例如时间戳,可通过suppressHydrationWarning={true}属性显式忽略该元素的 HydrationWarning(只是忽略警告,并不纠错,所以仍保留服务端渲染结果)。如果非要在服务端和客户端分别渲染不同的内容,建议先保证首次渲染内容一致,再通过更新来完成(当然,性能会稍差一点),例如:

class MyComponent extends React.Component {
  state = {
    isClient: false
  }

  render() {
    return this.state.isClient ? '渲染...客户端内容' : '渲染...服务端内容';
  }

  componentDidMount() {
    this.setState({
      isClient: true
    });
  }
}

SSR 模式下,服务端只执行 3 个生命周期函数

  • constructor
  • getDerivedStateFromProps
  • render

其余任何生命周期在服务端都不执行,包括getDerivedStateFromErrorcomponentDidCatch等错误处理 API

react ssr lifecycle

P.S.已经废弃的componentWillMountUNSAFE_componentWillMountgetDerivedStateFromPropsgetSnapshotBeforeUpdate互斥,若存在后一组新 API 中的任意一个,就不会调用前两个旧 API

With streaming rendering it's impossible to "call back" markup that has already been sent, and we opted to keep renderToString and renderToNodeStream's output identical.

为了支持流式渲染,同时保持 String API 与 Stream API 输出内容的一致性,牺牲了会引发渲染回溯的两大特性

  • Error Boundary:能够捕获子孙组件的运行时错误,并渲染一个降级 UI

  • Portal:能够将组件渲染到指定的任意 DOM 节点上,同时保留事件按组件层级冒泡

很容易理解,流式边渲染边响应,无法(回溯回去)修改已经发出去的内容,所以其它类似的场景也不支持,比如渲染过程中动态往head里插个stylescript标签

P.S.关于 SSR Error Boundary 的更多讨论,见componentDidCatch doesn't work in React 16's renderToString