Skip to content

CJY0208/santi

Repository files navigation

Santi

基于 create-react-appjsdom 的同构方案,SPA/预渲染/SSR 三位一体

SSR 功能基于 jsdom,每次渲染需启动 jsdom 沙盒,相对于 React 官方 renderToString 方案存在较大性能差异,因此使用 santi 时需要考虑使用缓存控制

目前主要针对低并发高可用性或高并发低可用性等低计算量场景

具体可参考压测表现


特性

  • 自动内联关键样式,快速呈现首屏(对比 Nextjs
  • SSR 无需关注服务端 / 客户端差异
  • SSR 使用沙盒渲染,无需关注同构时的内存泄漏问题
  • SPA、预渲染、SSR 功能渐进式开启或关闭
  • 性能尚可(搭配合理的缓存,单核心 500QPS + 20ms/AVG 响应)
  • SSR 页面级缓存,自由控制
  • SSR 组件级缓存
  • SSR 分片支持

在线示例

https://codesandbox.io/s/santi-demo-bl1f9

兼容性

React v16.8.0+

Preact v10+

需要 Hooks 支持

node v8+

需要 async/await 语法支持


起步

需要先使用 create-react-app 生成项目

或者直接参考完整示例项目 example

  1. 安装 santi 依赖

    npm install santi --save
    # or
    yarn add santi
  2. package.json 中增加 "config-overrides-path" 项,并替换 scripts 执行部分

    serve 命令用于启动构建模式后的 SSR 或静态资源代理服务

    NOTE:暂不支持 test 命令,请勿使用 react-scripts eject 命令

    /* package.json */
    {
      ...
      "scripts": {
        ...
    -   "start": "react-scripts start",
    +   "start": "santi start",
    -   "build": "react-scripts build",
    +   "build": "santi build"
    +   "serve": "santi serve",
        ...
       },
    + "config-overrides-path": "node_modules/santi/config",
      ...
    }
  3. 使用 santi.render 替代 ReactDOM.render

    - import { render } from 'react-dom
    + import { render } from 'santi'
    
    render(<App />, document.getElementById('root'))

主动声明渲染完成

santi 中,每一次 SSR 并不会像 nextjs 那样会在 getInitialProps 后自动完成,需要触发一个自定义事件 ssr-ready 来通知 jsdom 完成了渲染

若一次 SSR 渲染迟迟未收到 ssr-ready 事件,则默认在 1000ms 后强制采集结果并返回,超时时间可在配置文件中的 ssr.timeout 项更改

document.dispatchEvent(new Event('ssr-ready'))

这样做可以自由地确定完成 ssr 渲染的时机,以实现一些有趣的功能,例如让 ssr 允许处理异步载入的模块

在 santi 中,可以不需要手动触发这个自定义事件,触发过程被封装为了一些直接可用的方法:

  • api 形式的 ready 方法,它可以延迟触发

  • 组件形式的 Ready 组件,以及在组件 onMount 后立即发起 ready 的 Ready.OnMount 组件

    import { ready, Ready } from 'santi'
    
    ready() // 立即触发 ssr-ready 事件
    ready(1000) // 1s 后触发 ssr-ready 事件
    
    function TestReady() {
      const [ready, setReady] = useState(false)
    
      useEffect(() => {
        doSomething.then(() => {
          setReady(true)
        })
      }, [])
    
      return <Ready when={ready}>...</Ready>
    }
    
    function TestReadyOnMount() {
      return <Ready.OnMount>...</Ready.OnMount>
    }

在服务端准备数据

useState

santi 允许使用 hook 方式的 santi.useState 方法来初始化服务端数据,这个 hook 将会在 ssr 阶段触发,并同步到 csr 阶段,csr 阶段初始化时直接使用 ssr 阶段获得的数据

一般情况下,为了让 ssr 阶段的数据和 csr 阶段可以一一对应,需要给 useState 一个 key 值,用来做两侧数据的交接

import { useState, Ready } from 'santi'

function App() {
  const [value, setValue] = useState(undefined, 'App_ssrState')
  const [ready, setReady] = React.useState(false)

  React.useEffect(() => {
    doSomeAsyncWork().then(value => {
      setValue(value)
      setReady(true)
    })
  }, [])

  return <Ready when={ready}>State from SSR: {value}</Ready>
}

如果不希望主动声明这个 key 值,可以使用 withSanti HOC 包裹组件

import { withSanti, useState, Ready } from 'santi'

const App = withSanti(function App() {
  const [random, setRandom] = useState(Math.random())

  return <Ready.OnMount>State from SSR: {random}</Ready.OnMount>
})

getInitialProps

santi 也允许类似于 nextjs 中 getInitialProps 的操作,让数据在服务端就准备好,但使用方式上于 nextjs 有所不同

santi 中的 getInitialProps 使用的是 HOC 形式,渲染方式类似于 React.Suspence,将在异步任务完成后才加载组件,因此可以配合 Ready.OnMount 使用

getInitialProps 中包含了 withSanti 功能,因此其内部使用 santi.useState 不需要声明 key 值

import { getInitialProps, Ready } from 'santi'

const delay = time => new Promise(resolve => setTimeout(resolve, time))

const App = getInitialProps(async () => {
  await delay(200) // 模拟异步任务延时

  return {
    random: `ssrProp ${Math.random()}`
  }
})(function App({ random }) {
  return <Ready.OnMount>Prop from SSR: {random}</Ready.OnMount>
})

Santi 配置(页面缓存配置)

合法的 santi 配置文件为以下路径

  • santi.config.js 或 santi.config.ts
  • .santirc.js 或 .santirc.ts
  • config/index.js 或 config/index.ts

配置文件内容如下

const { addWebpackAlias } = require('customize-cra')

module.exports = {
  mode: 'ssr' // santi 模式,可选值为 'csr' | 'ssr'

  prerender: ['/', '/list'] // 构建阶段需要预渲染的路由

  ssr: {
    timeout: 1000, // 每个 ssr 任务等待 ssr-ready 的最长超时时间
    renderConfig: [ // 每个渲染请求的行为配置,如缓存等
      [
        // 使用 micromatch 进行路径匹配
        // https://github.com/micromatch/micromatch
        ['/', '/?**'],
        req => ({
          key: `${req.path}:${req.cookie.uid}`, // 缓存将受 cookie 中 uid 影响,不同 uid 缓存不同
          cache: {
            maxAge: 1000 // 每次对 / 路由的请求都将缓存 1s
          }
        })
      ],
      [
        '/list',
        {
          key: '/list',
          timeout: 2000, // 单独配置该请求的 ssr-ready 超时
          cache: true // 仅渲染一次后长期缓存
        }
      ],
      [
        '**', // 默认渲染配置
        {
          ssr: false // 不使用 ssr
        }
      ]
    ],
    cacheEngine: { // 可选自定义缓存引擎,接入 redis 缓存等,默认为 lru-cache 内存缓存
      get(key) {...},
      set(key, value maxAge) {...}
    }
  },

  // 代理部分参考 http-proxy-middleware
  // https://github.com/chimurai/http-proxy-middleware
  proxy: {
    '/api': 'http://www.somewhere.com'
  }

  // 基于 create-react-app,并使用 react-app-rewired 和 customize-cra 进行无 inject 定制 webpack
  // https://github.com/timarney/react-app-rewired
  // https://github.com/arackaf/customize-cra
  // webpack 部分对应 customize-cra 中的 override 函数
  webpack: [
    addWebpackAlias({
      // 使用 preact 代替 react
      // https://preactjs.com/guide/v10/switching-to-preact
      react: 'preact/compat',
      'react-dom': 'preact/compat'
    })
  ],

  // webpack 部分对应 customize-cra 中的 overrideDevServer 函数
  devServer: []
}

TODO

  • useState
  • getInitialProps
  • SSR 页面级缓存
  • SSR 组件级缓存
  • SSR 分片支持

FP/FMP/TTI Compare with Nextjs

55 节点数,无异步任务场景

Network CPU First Contentful Paint(s) First Meaningful Paint(s) Time to Interactive(s)
Nextjs Fast 3G Mobile (PC 4x Slowndown) 1.8 1.8 2.0
Santi Fast 3G Mobile (PC 4x Slowndown) 0.6 0.6 1.4
Nextjs Wifi PC 0.2 0.2 0.2
Santi Wifi PC 0.1 0.1 0.1

4710 节点数(55 节点 repeat 100x),无异步任务场景

Network CPU First Contentful Paint(s) First Meaningful Paint(s) Time to Interactive(s)
Nextjs Fast 3G Mobile (PC 4x Slowndown) 3.1 3.1 3.4
Santi Fast 3G Mobile (PC 4x Slowndown) 0.8 3.2 3.2
Nextjs Wifi PC 0.2 0.2 0.2
Santi Wifi PC 0.6 0.9 0.9

SSR Benchmark Compare with Nextjs

55 节点数,无异步任务场景,限定 2000 样本、i7-4790 基准测试

并发数 1

可用性 集群数 完成时间(秒) QPS 平均响应时间(秒)
Nextjs 全量计算 1 5.436 369.37 0.002707
Santi 全量计算 1 105.966 18.88 0.052970
Santi 缓存 100ms 1 5.906 339.54 0.002945
Nextjs 全量计算 6 5.705 352.00 0.002841
Santi 全量计算 6 107.991 18.52 0.053981
Santi 缓存 100ms 6 7.315 274.09 0.003648

并发数 10

可用性 集群数 完成时间(秒) QPS 平均响应时间(秒)
Nextjs 全量计算 1 2.316 874.68 0.011433
Santi 全量计算 1 31.884 62.87 0.159047
Santi 缓存 100ms 1 3.569 564.61 0.017711
Nextjs 全量计算 6 2.679 755.05 0.013244
Santi 全量计算 6 19.419 103.37 0.096742
Santi 缓存 100ms 6 3.972 507.34 0.019711

并发数 50

可用性 集群数 完成时间(秒) QPS 平均响应时间(秒)
Nextjs 全量计算 1 2.113 967.81 0.051663
Santi 全量计算 1 30.899 65.04 0.768779
Santi 缓存 100ms 1 2.446 829.93 0.060246
Nextjs 全量计算 6 2.24 910.84 0.054894
Santi 全量计算 6 12.956 155.70 0.321122
Santi 缓存 100ms 6 2.523 809.01 0.061804

并发数 100

可用性 集群数 完成时间(秒) QPS 平均响应时间(秒)
Nextjs 全量计算 1 2.174 949.62 0.105305
Santi 全量计算 1 31.02 65.08 1.536474
Santi 缓存 100ms 1 2.157 953.79 0.104845
Nextjs 全量计算 6 2.224 927.44 0.107824
Santi 全量计算 6 11.975 170.42 0.586769
Santi 缓存 100ms 6 2.061 1,015.84 0.098441

并发数 300

可用性 集群数 完成时间(秒) QPS 平均响应时间(秒)
Nextjs 全量计算 1 2.152 978.44 0.306611
Santi 全量计算 1 30.409 68.90 4.354413
Santi 缓存 100ms 1 1.557 1,367.29 0.219412
Nextjs 全量计算 6 2.139 966.40 0.310432
Santi 全量计算 6 12.361 195.62 1.533604
Santi 缓存 100ms 6 1.812 1,184.90 0.253185

并发数 500

可用性 集群数 完成时间(秒) QPS 平均响应时间(秒)
Nextjs 全量计算 1 2.423 987.80 0.506178
Santi 全量计算 1 31.313 70.84 7.058347
Santi 缓存 100ms 1 1.571 1,381.07 0.362038
Nextjs 全量计算 6 2.225 981.13 0.509618
Santi 全量计算 6 10.724 212.19 2.356404
Santi 缓存 100ms 6 1.692 1,273.01 0.392771

并发数 1000

可用性 集群数 完成时间(秒) QPS 平均响应时间(秒)
Nextjs 全量计算 1 2.197 993.31 1.006733
Santi 全量计算 1 31.05 82.12 12.176888
Santi 缓存 100ms 1 1.497 1,572.22 0.636043
Nextjs 全量计算 6 2.184 1,015.39 0.984847
Santi 全量计算 6 11.51 225.60 4.432673
Santi 缓存 100ms 6 1.591 1,495.70 0.668581

About

Isomorphic framework base on create-react-app and jsdom

Resources

Stars

Watchers

Forks

Packages

No packages published