Use Vite to build a highly available server-side rendering SSR project

In the very early days of web development, people were still using the ancient template syntax of JSP to write front-end pages, and then directly put the JSP file on the server, fill in the data on the server and render the complete page content, you can It is said that the practice of that era was natural server-side rendering. However, with the maturity of AJAX technology and the rise of various front-end frameworks (such as Vue and React), the development mode of front-end and back-end separation has gradually become the norm. The front-end is only responsible for the development of page UI and logic, while the server is only responsible for providing data interfaces. Page rendering under this development method is also called client side rendering (Client Side Render, referred to as CSR).

However, there are also certain problems in client-side rendering, such as slow loading of the first screen and not very friendly to SEO. Therefore, SSR (Server Side Render), that is, server-side rendering technology, emerged as the times require. While retaining the CSR technology stack, it also Can solve various problems of CSR.

1. Basic concept of SSR

First, let's analyze the problem of CSR. Its HTML product structure is generally as follows.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title></title>
  <link rel="stylesheet" href="xxx.css" />
</head>
<body>
  <!-- 一开始没有页面内容 -->
  <div id="root"></div>
  <!-- 通过 JS 执行来渲染页面 -->
  <script src="xxx.chunk.js"></script>
</body>
</html>

Next, let's briefly review the browser's rendering process. The following is a simple schematic diagram.
insert image description here

When the browser gets the above HTML content, it can't actually render the complete page content, because there is basically only an empty div node in the body at this time, and no real page content is filled in. Then the browser starts to download and execute the JS code, and the complete page can only be rendered after the frame initialization, data request, DOM insertion and other operations. That is, the complete page content in the CSR is essentially rendered after the execution of the JS code. This causes problems in two ways:

  • The loading speed of the first screen is relatively slow . The loading of the first screen depends on the execution of JS. Downloading and executing JS may be very time-consuming operations, especially in some scenarios with poor network or performance-sensitive low-end machines.
  • Not SEO (search engine optimization) friendly . The HTML of the page has no specific page content, so search engine crawlers cannot obtain keyword information, which affects the ranking of the website.

So how does SSR solve these problems? First of all, in the SSR scenario, the server generates complete HTML content and returns it directly to the browser. The browser can render the complete first-screen content according to the HTML without relying on JS loading, which can to a certain extent It reduces the rendering time of the first screen, and on the other hand, it can also display the complete page content to the crawlers of search engines, which is conducive to SEO.

Of course, SSR can only generate the content and structure of the page, and cannot complete event binding. Therefore, it is necessary to execute the JS script of CSR in the browser to complete event binding and make the page interactive. This process is called hydrate (translated as water injection or activation). At the same time, an application that uses server-side rendering + client-side hydrate is also called an isomorphic application.

2. SSR life cycle

We said that SSR will render the complete HTML content in advance on the server side, so how does this work?

First of all, it is necessary to ensure that the front-end code can be executed normally after being compiled and placed on the server. Secondly, the front-end components are rendered on the server to generate and assemble the HTML of the application. This involves the two major life cycles of SSR applications: build time and runtime, we might as well sort it out carefully.

build time

The construction phase of SSR mainly does the following things:

  1. Fix module loading issues . In addition to the original construction process, the SSR construction process needs to be added. Specifically, we need to generate another product in CommonJS format so that it can be loaded normally in Node.js. Of course, as the support of Node.js itself for ESM becomes more and more mature, we can also reuse the code in the front-end ESM format. This is also the idea of ​​Vite's SSR construction in the development stage.
    insert image description here

  2. Remove the import of style code . Directly importing a line of css is actually impossible on the server side, because Node.js cannot parse the content of CSS. The exception is the case of CSS Modules, as follows:

import styles from './index.module.css'


//styles 是一个对象,如{ "container": "xxx" },而不是 CSS 代码
console.log(styles)

3. Rely on externalization (external) . For some third-party dependencies, we don't need to use the built version, but read directly from node_modules, such as react-dom, so that these dependencies will not be built during the SSR build process, thus greatly speeding up Construction of SSRs.

Runtime

For the runtime of SSR, it can generally be divided into relatively fixed life cycle stages. Simply put, it can be organized into the following core stages:
insert image description here

These stages are explained in detail below:

  1. Load the SSR entry module . At this stage, we need to determine the entry of the SSR build product, that is, where is the entry of the component, and load the corresponding module.
  2. Perform data prefetching . At this time, the Node side will query the database or network request to obtain the data required by the application.
  3. Render the component . This stage is the core of SSR, which mainly renders the components loaded in step 1 into HTML strings or Streams.
  4. HTML splicing . After the component is rendered, we need to concatenate the complete HTML string and return it to the browser as a response.

It can be found that SSR can only be realized by the cooperation between build and runtime. In other words, build tools alone are not enough. Therefore, developing a Vite plug-in cannot strictly implement the SSR capability. We need to make some changes to the build process of Vite. Some overall adjustments and adding some server-side runtime logic can be realized.

3. Building an SSR project based on Vite

3.1 SSR build API

How does Vite support SSR construction as a construction tool? In other words, how does it allow the front-end code to run successfully in Node.js?

Here are two cases to explain. In the development environment, Vite still adheres to the concept of ESM module loading on demand, that is, no-bundle, and provides ssrLoadModule API externally. You can pass the path of the entry file to ssrLoadModule without needing to package the project:

// 加载服务端入口模块
const xxx = await vite.ssrLoadModule('/src/entry-server.tsx')

In the production environment, Vite will package by default, and output the product in CommonJS format for SSR construction. We can add similar build instructions to package.json:

{
  "build:ssr": "vite build --ssr 服务端入口路径"
}

In this way, Vite will package a build product specifically for SSR. It can be seen that Vite has provided us with out-of-the-box solutions for most of the things in SSR construction.

3.2 Project Construction

Next, officially start the construction of the SSR project. You can initialize a react+ts project through scaffolding. The command is as follows.

npm init vite
npm i

Open the project, delete the src/main.ts that comes with the project, and create two entry files entry-client.tsx and entry-server.tsx in the src directory. Among them, the code of entry-client.tsx is as follows:

// entry-client.ts
// 客户端入口文件
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'


ReactDOM.hydrate(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

The code of entry-server.ts is as follows:

// entry-server.ts
// 导出 SSR 组件入口
import App from "./App";
import './index.css'


function ServerEntry(props: any) {
  return (
    <App/>
  );
}


export { ServerEntry };

Next, we take the Express framework as an example to implement the Node backend service, and the subsequent SSR logic will be connected to this service. Of course you need to install the following dependencies:

npm i express -S
npm i @types/express -D

Next, create a new ssr-server/index.ts file in the src directory, the code is as follows:

// src/ssr-server/index.ts
// 后端服务
import express from 'express';


async function createServer() {
  const app = express();
  
  app.listen(3000, () => {
    console.log('Node 服务器已启动~')
    console.log('http://localhost:3000');
  });
}


createServer();

Then, add the following scripts script in package.json:

{
  "scripts": {
    // 开发阶段启动 SSR 的后端服务
    "dev": "nodemon --watch src/ssr-server --exec 'esno src/ssr-server/index.ts'",
    // 打包客户端产物和 SSR 产物
    "build": "npm run build:client && npm run build:server",
    "build:client": "vite build --outDir dist/client",
    "build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
    // 生产环境预览 SSR 效果
    "preview": "NODE_ENV=production esno src/ssr-server/index.ts"
  },
}

Among them, two additional tools are needed in the project, let me explain to you:

  • nodemon: A tool that monitors file changes and automatically restarts Node services.
  • esno: A tool similar to ts-node, used to execute ts files, the bottom layer is based on Esbuild.

Let's install these two plugins first:

npm i esno nodemon -D

Now, the basic project skeleton has been built, and then we only need to focus on the implementation logic of the SSR runtime.

3.3 SSR runtime implementation

As a special back-end service, SSR can be encapsulated into a middleware form, which is much more convenient to use later. The code is as follows:

import express, { RequestHandler, Express } from 'express';
import { ViteDevServer } from 'vite';


const isProd = process.env.NODE_ENV === 'production';
const cwd = process.cwd();


async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  let vite: ViteDevServer | null = null;
  if (!isProd) { 
    vite = await (await import('vite')).createServer({
      root: process.cwd(),
      server: {
        middlewareMode: 'ssr',
      }
    })
    // 注册 Vite Middlewares
    // 主要用来处理客户端资源
    app.use(vite.middlewares);
  }
  return async (req, res, next) => {
    // SSR 的逻辑
    // 1. 加载服务端入口模块
    // 2. 数据预取
    // 3. 「核心」渲染组件
    // 4. 拼接 HTML,返回响应
  };
}


async function createServer() {
  const app = express();
  // 加入 Vite SSR 中间件
  app.use(await createSsrMiddleware(app));


  app.listen(3000, () => {
    console.log('Node 服务器已启动~')
    console.log('http://localhost:3000');
  });
}


createServer();

Next, we focus on the logical implementation of the SSR in the middleware. First, the first step is to load the server entry module. The code is as follows:

async function loadSsrEntryModule(vite: ViteDevServer | null) {
  // 生产模式下直接 require 打包后的产物
  if (isProd) {
    const entryPath = path.join(cwd, 'dist/server/entry-server.js');
    return require(entryPath);
  } 
  // 开发环境下通过 no-bundle 方式加载
  else {
    const entryPath = path.join(cwd, 'src/entry-server.tsx');
    return vite!.ssrLoadModule(entryPath);
  }
}

Among them, the logic in the middleware is as follows:

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  // 省略前面的代码
  return async (req, res, next) => {
    const url = req.originalUrl;
    // 1. 服务端入口加载
    const { ServerEntry } = await loadSsrEntryModule(vite);
    // ...
  }
}

Next, let's implement the data prefetching operation on the server side. You can add a simple function to obtain data in entry-server.tsx. The code is as follows:

export async function fetchData() {
  return { user: 'xxx' }
}

Next, the data prefetching operation can be completed in the SSR middleware.

// src/ssr-server/index.ts
async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  // 省略前面的代码
  return async (req, res, next) => {
    const url = req.originalUrl;
    // 1. 服务端入口加载
    const { ServerEntry, fetchData } = await loadSsrEntryModule(vite);
    // 2. 预取数据
    const data = await fetchData();
  }
}

Next, we enter the core component rendering stage:

// src/ssr-server/index.ts
import { renderToString } from 'react-dom/server';
import React from 'react';


async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  // 省略前面的代码
  return async (req, res, next) => {
    const url = req.originalUrl;
    // 1. 服务端入口加载
    const { ServerEntry, fetchData } = await loadSsrEntryModule(vite);
    // 2. 预取数据
    const data = await fetchData();
    // 3. 组件渲染 -> 字符串
    const appHtml = renderToString(React.createElement(ServerEntry, { data }));
  }
}

Since we got the entry component after the first step, we can now call the renderToStringAPI of the front-end framework to render the component as a string, and the specific content of the component is thus generated. Next, we also need to provide corresponding slots in the HTML under the root directory to facilitate content replacement.

// index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <div id="root"><!-- SSR_APP --></div>
    <script type="module" src="/src/entry-client.tsx"></script>
    <!-- SSR_DATA -->
  </body>
</html>

Next, we supplement the logic of HTML splicing in the SSR middleware.

// src/ssr-server/index.ts
function resolveTemplatePath() {
  return isProd ?
    path.join(cwd, 'dist/client/index.html') :
    path.join(cwd, 'index.html');
}


async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  // 省略之前的代码
  return async (req, res, next) => {
    const url = req.originalUrl;
    // 省略前面的步骤
    // 4. 拼接完整 HTML 字符串,返回客户端
    const templatePath = resolveTemplatePath();
    let template = await fs.readFileSync(templatePath, 'utf-8');
    // 开发模式下需要注入 HMR、环境变量相关的代码,因此需要调用 vite.transformIndexHtml
    if (!isProd && vite) {
      template = await vite.transformIndexHtml(url, template);
    }
    const html = template
      .replace('<!-- SSR_APP -->', appHtml)
      // 注入数据标签,用于客户端 hydrate
      .replace(
        '<!-- SSR_DATA -->',
        `<script>window.__SSR_DATA__=${JSON.stringify(data)}</script>`
      );
    res.status(200).setHeader('Content-Type', 'text/html').end(html);
  }
}

In the logic of splicing HTML, in addition to adding the specific content of the page, we also inject a script tag that mounts global data. What is this for?

We mentioned in the basic concept of SSR that in order to activate the interactive function of the page, we need to execute the JavaScript code of the CSR to perform the hydrate operation, and when the client hydrates, it needs to synchronize the prefetched data with the server to ensure that the page The rendered result is consistent with server-side rendering, so the data script tag we just injected comes in handy. Since the data prefetched by the server is mounted on the global window, we can get this data in the entry-client.tsx, which is the client rendering entry, and hydrate it.

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'


// @ts-ignore
const data = window.__SSR_DATA__;


ReactDOM.hydrate(
  <React.StrictMode>
    <App data={data}/>
  </React.StrictMode>,
  document.getElementById('root')
)

Now, we have basically developed the logic of the SSR core, and then execute the npm run dev command to start the project.
insert image description here

After opening the browser and viewing the source code of the page, you can find that the HTML generated by SSR has been successfully returned, as shown in the figure below.
insert image description here

3.4 CSR Resource Processing in Production Environment

If we now execute npm run build and npm run preview to preview the production environment, we will find that SSR can return content normally, but all static resources and CSR codes are invalid.
insert image description here

However, there is no such problem in the development stage. This is because the middleware of Vite Dev Server has already helped us handle the static resources in the development stage, and all the resources in the production environment have been packaged. We need to enable a separate static resource service to host these resources.

For this kind of problem, we can use serve-static middleware to complete this service. First, startle and install the corresponding third-party package:

npm i serve-static -S

Next, we go to the server to register.

// 过滤出页面请求
function matchPageUrl(url: string) {
  if (url === '/') {
    return true;
  }
  return false;
}


async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  return async (req, res, next) => {
    try {
      const url = req.originalUrl;
      if (!matchPageUrl(url)) {
        // 走静态资源的处理
        return await next();
      }
      // SSR 的逻辑省略
    } catch(e: any) {
      vite?.ssrFixStacktrace(e);
      console.error(e);
      res.status(500).end(e.message);
    }
  }
}


async function createServer() {
  const app = express();
  // 加入 Vite SSR 中间件
  app.use(await createSsrMiddleware(app));


  // 注册中间件,生产环境端处理客户端资源
  if (isProd) {
    app.use(serve(path.join(cwd, 'dist/client')))
  }
  // 省略其它代码
}

In this way, we have solved the problem of static resource failure in the production environment. However, under normal circumstances, we will upload the static resources to the CDN, and configure Vite's base as a domain name prefix, so that we can directly access the static resources through the CDN without adding server-side processing.

4. Engineering issues

Above we have basically realized the construction and runtime functions of the SSR core, and can initially run a Vite-based SSR project, but there are still many engineering problems that need our attention in actual scenarios.

4.1 Routing Management

In the SPA scenario, there are generally different routing management solutions for different front-end frameworks, such as vue-router in Vue and react-router in React. But in the final analysis, the functions performed by the routing scheme in the SSR process are similar.

  • Tells the framework which routes to render at the moment. In Vue, we can use router.push to determine the route to be rendered, and in React, use StaticRouter with the location parameter to complete.
  • Set the base prefix. Specifies the prefix of the path, such as the base parameter in vue-router and the basename of the StaticRouter component in react-router.

4.2 State Management

For global state management, there are different ecology and solutions for different frameworks, such as Vuex and Pinia in Vue, Redux and Recoil in React. The usage of each state management tool is not the focus of this article. The idea of ​​connecting to SSR is relatively simple. In the data prefetch stage, initialize the store on the server side, store the asynchronously obtained data in the store, and then transfer the data from the store to the HTML stage. Take it out and put it in the data script tag, and finally access the prefetched data through the window when the client is hydrated.

4.3 CSR downgrade

In some extreme cases, we need to fall back to CSR, which is client-side rendering. Generally speaking, the following downgrade scenarios are included:

  • The server failed to prefetch data and needs to downgrade to the client to obtain data.
  • The server has an exception, and needs to return the bottom-up CSR template and completely downgrade it to a CSR.
  • For local development and debugging, sometimes it is necessary to skip SSR and only perform CSR.

For the first case, there needs to be logic for re-acquiring data in the client entry file, and we can make the following additions.

// entry-client.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'


async function fetchData() {
  // 客户端获取数据
}




async fucntion hydrate() {
  let data;
  if (window.__SSR_DATA__) {
    data = window.__SSR_DATA__;
  } else {
    // 降级逻辑 
    data = await fetchData();
  }
  // 也可简化为 const data = window.__SSR_DATA__ ?? await fetchData();
  ReactDOM.hydrate(
    <React.StrictMode>
      <App data={data}/>
    </React.StrictMode>,
    document.getElementById('root')
  )
}

For the second scenario, that is, the server executes an error, we can add try/catch logic to the previous SSR middleware logic.

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  return async (req, res, next) => {
    try {
      // SSR 的逻辑省略
    } catch(e: any) {
      vite?.ssrFixStacktrace(e);
      console.error(e);
      // 在这里返回浏览器 CSR 模板内容
    }
  }
}

For the third case, we can force skip SSR by passing the url query parameter of ?csr, and add the following logic in the SSR middleware.

async function createSsrMiddleware(app: Express): Promise<RequestHandler> {
  return async (req, res, next) => {
    try {
      if (req.query?.csr) {
        // 响应 CSR 模板内容
        return;
      }
      // SSR 的逻辑省略
    } catch(e: any) {
      vite?.ssrFixStacktrace(e);
      console.error(e);
    }
  }
}

4.4 Browser API compatibility

Because APIs such as window and document in the browser cannot be used in Node.js, once such APIs are executed on the server side, the following error will be reported:

image.png

For this problem, we can first use the Vite built-in environment variable import.meta.env.SSR to determine whether it is in the SSR environment, so as to avoid the browser API appearing on the server side of the business code.

if (import.meta.env.SSR) {
  // 服务端执行的逻辑
} else {
  // 在此可以访问浏览器的 API
}

Of course, we can also inject browser APIs into Node through polyfill, so that these APIs can run normally and solve the above problems. I recommend using a relatively mature polyfill library jsdom, which is used as follows:

const jsdom = require('jsdom');
const { window } = new JSDOM(`<!DOCTYPE html><p>Hello world</p>`);
const { document } = window;
// 挂载到 node 全局
global.window = window;
global.document = document;

4.5 Custom Head

In the process of SSR, although we can decide the content of the component, that is

The content in the container div, but for the content of the head in the HTML we cannot decide based on the internal state of the component. However, the react-helmet in the React ecosystem and the vue-meta library in the Vue ecosystem are designed to solve such problems, allowing us to directly write some Head tags in the components, and then get the internal state of the components on the server side.

Take react-helmet example to illustrate:

// 前端组件逻辑
import { Helmet } from "react-helmet";


function App(props) {
  const { data } = props;
  return {
    <div>
       <Helmet>
        <title>{ data.user }的页面</title>
        <link rel="canonical" href="http://mysite.com/example" />
      </Helmet>
    </div>
  }
}
// 服务端逻辑
import Helmet from 'react-helmet';


// renderToString 执行之后
const helmet = Helmet.renderStatic();
console.log("title 内容: ", helmet.title.toString());
console.log("link 内容: ", helmet.link.toString())

After starting the service and visiting the page, we can find that the terminal can print out the information we want. In this way, we can determine the Head content according to the state of the component, and then insert the content into the template during the splicing HTML stage.

4.6 Streaming rendering

The bottom layer of different front-end frameworks has realized the ability of streaming rendering, that is, responding while rendering, instead of waiting for the entire component tree to be rendered before responding. This can make the response reach the browser in advance and improve the loading performance of the first screen. The renderToNodeStream in Vue and the renderToNodeStream in React both realize the ability of streaming rendering, and the general usage is as follows:

import { renderToNodeStream } from 'react-dom/server';


// 返回一个 Nodejs 的 Stream 对象
const stream = renderToNodeStream(element);
let html = ''


stream.on('data', data => {
  html += data.toString()
  // 发送响应
})


stream.on('end', () => {
  console.log(html) // 渲染完成
  // 发送响应
})


stream.on('error', err => {
  // 错误处理
})

However, while streaming rendering improves the performance of the first screen, it also brings us some restrictions: If we need to fill in some content related to the component state in HTML, streaming rendering cannot be used. For example, for the custom head content in react-helmet, even if the head information is collected when the component is rendered, in streaming rendering, the head part of the HTML has been sent to the browser at this time, and this part of the response content cannot be changed , so react-helmet will fail during SSR.

4.7 SSR cache

SSR is a typical CPU-intensive operation. In order to reduce the load of the online machine as much as possible, setting the cache is a very important link. When SSR is running, the cached content can be divided into several parts:

  • File read cache . Avoid repeated disk read operations as much as possible, and reuse the cached results as much as possible for each disk IO. As shown in the following code:
     function createMemoryFsRead() {
  const fileContentMap = new Map();
  return async (filePath) => {
    const cacheResult = fileContentMap.get(filePath);
    if (cacheResult) {
      return cacheResult;
    }
    const fileContent = await fs.readFile(filePath);
    fileContentMap.set(filePath, fileContent);
    return fileContent;
  }
}


const memoryFsRead = createMemoryFsRead();
memoryFsRead('file1');
// 直接复用缓存
memoryFsRead('file1');
  • Prefetch data cache . For some interface data with low real-time performance, we can adopt a caching strategy to reuse the results of prefetching data when the same request comes in next time, so that various IO consumption in the process of prefetching data can also be reduced to a certain extent. Reduce time to first screen.
  • HTML rendering cache . The spliced ​​HTML content is the focus of caching. If this part can be cached, after the next cache hit, a series of consumption such as renderToString and HTML splicing can be saved, and the performance benefit of the server will be more obvious.

For the above cache content, the specific cache location can be:

  • server memory . If it is placed in the memory, it is necessary to consider the cache elimination mechanism to prevent service downtime caused by excessive memory. A typical cache elimination solution is lru-cache (based on the LRU algorithm).
  • Redis database. It is equivalent to processing cache with the design idea of ​​traditional back-end server.
  • CDN service . We can cache the page content on the CDN service, and when the same request comes in next time, use the cached content on the CDN instead of consuming the resources of the source server.

4.8 Performance Monitoring

In actual SSR projects, we often encounter some SSR online performance problems. Without a complete performance monitoring mechanism, it will be difficult to find and troubleshoot problems. For SSR performance data, there are some common indicators:

  • SSR product loading time
  • Data prefetching time
  • When the component renders
  • The complete time from the server receiving the request to the response
  • SSR cache hit
  • SSR success rate, error log

We can use the perf_hooks tool to complete data collection, as shown in the following code:

import { performance, PerformanceObserver } from 'perf_hooks';


// 初始化监听器逻辑
const perfObserver = new PerformanceObserver((items) => {
  items.getEntries().forEach(entry => { 
    console.log('[performance]', entry.name, entry.duration.toFixed(2), 'ms');
  });
  performance.clearMarks();
});


perfObserver.observe({ entryTypes: ["measure"] })


// 接下来我们在 SSR 进行打点
// 以 renderToString  为例
performance.mark('render-start');
// renderToString 代码省略
performance.mark('render-end');
performance.measure('renderToString', 'render-start', 'render-end');

Then we start the service and visit, and we can see the RBI log information. Similarly, we can collect the indicators of other stages through the above methods as performance logs; on the other hand, in the production environment, we generally need to combine specific performance monitoring platforms to manage and report the above indicators. Complete the online SSR performance monitoring service.

4.9 SSG/ISR/SPR

Sometimes for some static sites (such as blogs, documents), no dynamically changing data is involved, so we don't need to use server-side rendering. At this point, you only need to generate complete HTML for deployment during the construction phase. This method of generating HTML during the construction phase is also called SSG (Static Site Generation, static site generation).

The biggest difference between SSG and SSR is that the time to generate HTML has changed from SSR runtime to build time, but the core life cycle process has not changed:

image.png

The following is a simple implementation code:

// scripts/ssg.ts
// 以下的工具函数均可以从 SSR 流程复用
async function ssg() {
  // 1. 加载服务端入口
  const { ServerEntry, fetchData } = await loadSsrEntryModule(null);
  // 2. 数据预取
  const data = await fetchData();
  // 3. 组件渲染
  const appHtml = renderToString(React.createElement(ServerEntry, { data }));
  // 4. HTML 拼接
  const template = await resolveTemplatePath();
  const templateHtml = await fs.readFileSync(template, 'utf-8');
  const html = templateHtml
  .replace('<!-- SSR_APP -->', appHtml)
  .replace(
    '<!-- SSR_DATA -->',
    `<script>window.__SSR_DATA__=${JSON.stringify(data)}</script>`
  ); 
  // 最后,我们需要将 HTML 的内容写到磁盘中,将其作为构建产物
  fs.mkdirSync('./dist/client', { recursive: true });
  fs.writeFileSync('./dist/client/index.html', html);
}


ssg();

Then, add such a piece of npm scripts to package.json to use it.

{
  "scripts": {
    "build:ssg": "npm run build && NODE_ENV=production esno scripts/ssg.ts"  
  }
}

In this way, we initially realized the logic of SSG. Of course, in addition to SSG, there are some other rendering modes circulating in the industry, such as SPR and ISR, which sound taller, but in fact they are just new functions derived from SSR and SSG. Here is a brief explanation for you:

  • SPR stands for Serverless Pre Render, which deploys SSR services in a Serverless (FaaS) environment to realize automatic expansion and contraction of server instances and reduce the cost of server operation and maintenance.
  • ISR stands for Incremental Site Rendering, which moves part of the SSG logic from construction time to SSR runtime, and solves the problem of time-consuming SSG construction for a large number of pages.

Guess you like

Origin blog.csdn.net/xiangzhihong8/article/details/131426226