UNPKG

santi

Version:

Isomorphic framework for base on create-react-app and jsdom

390 lines (288 loc) 14.8 kB
# Santi ![](https://visitor-badge.glitch.me/badge?page_id=cjy0208.santi) 基于 [create-react-app](https://create-react-app.dev/) [jsdom](https://github.com/jsdom/jsdom) 的同构方案,SPA/预渲染/SSR 三位一体 SSR 功能基于 jsdom,每次渲染需启动 jsdom 沙盒,相对于 React 官方 renderToString 方案存在较大性能差异,因此使用 santi 时需要考虑使用[缓存控制](#config) 目前主要针对低并发高可用性或高并发低可用性等低计算量场景 具体可参考[压测表现](#benchmark) --- ## 特性 - [x] 自动内联关键样式,快速呈现首屏([对比 Nextjs](#fpspeed)) - [x] SSR 无需关注服务端 / 客户端差异 - [x] SSR 使用沙盒渲染,无需关注同构时的内存泄漏问题 - [x] SPA、预渲染、SSR 功能渐进式开启或关闭 - [x] 性能尚可(搭配合理的缓存,单核心 500QPS + 20ms/AVG 响应) - [x] SSR 页面级缓存,自由控制 - [ ] SSR 组件级缓存 - [ ] SSR 分片支持 --- ## 在线示例 https://codesandbox.io/s/santi-demo-bl1f9 ## 兼容性 React v16.8.0+ Preact v10+ 需要 Hooks 支持 node v8+ 需要 async/await 语法支持 --- ## 起步 > 需要先使用 [create-react-app](https://create-react-app.dev/docs/getting-started) 生成项目 > > 或者直接参考完整示例项目 [example](https://github.com/CJY0208/santi/tree/master/example) 1. 安装 santi 依赖 ```bash npm install santi --save # or yarn add santi ``` 2. `package.json` 中增加 `"config-overrides-path"` 项,并替换 scripts 执行部分 `serve` 命令用于启动构建模式后的 SSR 或静态资源代理服务 **NOTE:暂不支持 `test` 命令,请勿使用 `react-scripts eject` 命令** ```diff /* 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` ```diff - 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 后强制采集结果并返回,超时时间可在[配置文件](#config)中的 ssr.timeout 项更改 ```js document.dispatchEvent(new Event('ssr-ready')) ``` 这样做可以自由地确定完成 ssr 渲染的时机,以实现一些有趣的功能,例如让 ssr 允许处理异步载入的模块 santi 中,可以不需要手动触发这个自定义事件,触发过程被封装为了一些直接可用的方法: - api 形式的 ready 方法,它可以延迟触发 - 组件形式的 Ready 组件,以及在组件 onMount 后立即发起 ready Ready.OnMount 组件 ```jsx 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 值,用来做两侧数据的交接 ```jsx 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 包裹组件 ```jsx 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 ```jsx 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> }) ``` --- ## <div id="config" /> Santi 配置(页面缓存配置) 合法的 santi 配置文件为以下路径 - santi.config.js santi.config.ts - .santirc.js .santirc.ts - config/index.js config/index.ts 配置文件内容如下 ```js 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 - [x] ~~useState~~ - [x] ~~getInitialProps~~ - [x] ~~SSR 页面级缓存~~ - [ ] SSR 组件级缓存 - [ ] SSR 分片支持 --- ## <div id="fpspeed" /> 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 | --- ## <div id="benchmark" />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 |