1. Related concepts

  • CSR: Client Side Render. The rendering process is all handled by the browser, and the server does not participate in any rendering.
  • SSR: Server Side Render. The DOM tree is generated on the server and returned to the front end. That is, the content of the current page is generated by the server and returned to the browser for rendering.
  • Isomorphism: the combination of client-side rendering and server-side rendering. It is performed once on the server side to achieve server-side rendering (first screen straight out) and again on the client side to take over page interaction (binding events). The core solution is SEO and slow first-screen rendering.

2. SSR (server rendering) technology implementation scheme

  • Server-side rendering scheme using next.js/nuxt.js
  • Implement server rendering of VUE project using Node + VUe-server-renderer
  • Using the + React renderToStaticMarkup/renderToString React server rendering of the project
  • Using template engines to implement SSR (e.g. Ejs, JADE, PUG, etc.)

My department based on vue Nuxt framework to implement the SSR isomorphism rendering, but Nuxt did not provide the corresponding degradation strategy, when the node server request appeared accidental error (interface service hang up), it should be in the first screen rendering module for countless according to the blank, and no try the request again in the process of the client activated interface; Double eleven high flow, there is human “operation and maintenance” helpless, imagine other small partners with the object, eating hot pot, singing, you do in front of the computer holding uneasy mood staring at the monitoring system…. We need a downgrade just in case.

3. Vue-ssr implementation process

Before we start the downgrading program, we must first have a certain understanding of SSR principles. Let’s take vUE as an example:

As shown in the figure above, there are two entry files, Server Entry and Client Entry, which are respectively packaged by Webpack into Server Bundle for the Server and Client Bundle for the Client. Server: When Node Server receives a request from the client, the BundleRenderer reads the Server Bundle and executes it. The Server Bundle implements the data prefetch and mounts the Vue instance that fills the data on the HTML template. BundleRenderer then renders the HTML as a string and finally returns the complete HTML to the client. Client: After the browser receives the HTML, the Client Bundle loads and mounts the Vue instance to the static HTML returned by the server via app.$mount(‘#app’). Such as:

<div id="app" data-server-rendered="true">
Copy the code

Data server – rendered special attributes, let the client Vue know this part of the HTML is the Vue on the server side rendering, and should be to activate the mode (Hydration:ssr.vuejs.org/zh/hydratio…

4. Performance optimization

The purpose of degradation is to prevent high load or failure of the service, so before this, you can also do some optimization through code implementation, the following is a brief introduction to several general optimization methods

  • Reduce the number of DOM renderings on the server
  1. When the page is very long, such as our common home page and product details page, there will be recommended product flow, evaluation, product introduction and other modules that do not appear on the first screen at the bottom, and there is no need to perform rendering at the server side. At this time, the slot system of Vue and is of built-in component can be combined. The vUE SSR server can execute only beforeCreate and Created life cycle, encapsulate custom components, and mount the wrapped components to the COMPONENT is attribute when the component mounted.
  2. The vUE advanced asynchronous component encapsulates the lazy loading method, which is loaded only when the module reaches the specified visual area;
function asyncComponent({componentFactory, loading = 'div', loadingData = 'loading', errorComponent, rootMargin = '0px',retry= 2}) { let resolveComponent; return () => ({ component: new Promise(resolve => resolveComponent = resolve), loading: { mounted() { const observer = new IntersectionObserver(([entries]) => { if (! entries.isIntersecting) return; observer.unobserve(this.$el); let p = Promise.reject(); for (let i = 0; i < retry; i++) { p = p.catch(componentFactory); } p.then(resolveComponent).catch(e => console.error(e)); }, { root: null, rootMargin, threshold: [0] }); observer.observe(this.$el); }, render(h) { return h(loading, loadingData); }, }, error: errorComponent, delay: 200 }); } export default { install: (Vue, option) => { Vue.prototype.$loadComponent = componentFactory => { return asyncComponent(Object.assign(option, { componentFactory })) } } }Copy the code

  • Enabling Multiple Processes

Node.js is a single-process, single-thread model. Based on event-driven, asynchronous, non-blocking mode, it can be applied to high concurrency scenarios, avoiding the resource overhead caused by thread creation and context switching between threads. However, in the case of a large number of computing and CPU time-consuming operations, threads cannot be enabled to utilize CPU multi-core resources, but multi-process can be enabled to utilize server multi-core resources.

  1. A single Node.js instance runs in a single thread. In order to make full use of the multi-core system, it is sometimes necessary to enable a group of Node.js processes to handle load tasks. Cluster manages multiple processes in the master-slave mode. The master process is responsible for starting and scheduling the worker process, and the worker process is responsible for processing requests and other logic.

  2. Production environments generally use PM2 to maintain Node projects. If you control the communication between processes, let each process handle its own logic, and start the cluster by writing node scripts, pM2 is better from a robust point of view.

  • Open the cache
  1. Page-level caching: Lru-cache is used to Cache the currently requested resource while creating the Render instance.
  2. Component-level caching: Cacheable components must define a unique name option. By using a unique name, each cache key corresponds to a component. If the Renderer makes a cache hit during component rendering, it simply reuses the cached results of the entire subtree.
  3. Distributed cache: SSR applications are deployed in a multi-service and multi-process environment. The cache under the process is not shared, resulting in low cache matching efficiency. You can use Redis to share the cache between multiple processes

5. Project downgrade and transformation

The migration of business logic and the emergence of server-side rendering models for various MV* frameworks make the node-based front-end SSR strategy more dependent on server performance. The first screen display performance and Node service stability directly affect user experience. Node as a server language, the overall performance of Node is still not well tuned compared to older server languages such as Java and PHP. Although there is sentry alarm platform to timely notify the occurrence of errors, but also to consider the implementation of the corresponding disaster recovery scheme, otherwise how to trust the large traffic to it. In order to downgrade SSR to CSR, we need to package two HTML files, one for server rendering plugin createBundleRenderer as a template to pass in and output the rendered HTML fragments, and the other one as a static template for client rendering. When the server rendering fails or a degrade operation is triggered, the client code re-executes the component’s Async method to prefetch data.

  • Webpack.base. js In the common packaging configuration, you need to configure the location of the packaged file, the Loader to be used, and the Plugin for common use
// build/webpack.base.js const path = require('path') const VueLoaderPlugin = require('vue-loader/lib/plugin') const resolve = dir => path.resolve(__dirname, dir) module.exports = { output: { filename: '[name].bundle.js', path: resolve('.. / dist ')}, / / extension resolve: {extensions: [' js', 'vue', 'CSS', 'JSX']}, the module: {rules: [{$/ test: / \. CSS, use: ['vue-style-loader', 'css-loader'] }, // ..... { test: /\.js$/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } }, exclude: /node_modules/ }, { test: /\.vue$/, use: 'vue-loader' }, ] }, plugins: [ new VueLoaderPlugin(), ] }Copy the code

  • webpack.client.js
// build/webpack.client.js const webpack = require('webpack') const {merge} = require('webpack-merge'); const HtmlWebpackPlugin = require('html-webpack-plugin') const VueSSRClientPlugin = require('vue-server-renderer/client-plugin') const path = require('path') const resolve = dir => path.resolve(__dirname,  dir) const base = require('./webpack.base') const isProd = process.env.NODE_ENV === 'production' module.exports = merge(base, { entry: { client: resolve('.. /src/entry-client.js') }, plugins: [ new VueSSRClientPlugin(), new HtmlWebpackPlugin({ filename: 'index.csr.html', template: resolve('../public/index.csr.html') }) ] })Copy the code

  • webpack.server.js
// build/webpack.server.js const {merge} = require('webpack-merge'); const HtmlWebpackPlugin = require('html-webpack-plugin') const VueSSRServerPlugin = require('vue-server-renderer/server-plugin') const path = require('path') const resolve = dir => path.resolve(__dirname,  dir) const base = require('./webpack.base') module.exports = merge(base, { entry: { server: resolve('.. /src/entry-server.js') }, target:'node', output:{ libraryTarget:'commonjs2' }, plugins: [ new VueSSRServerPlugin(), new HtmlWebpackPlugin({ filename: 'index.ssr.html', template: resolve('../public/index.ssr.html'), minify: false, excludeChunks: ['server'] }) ] })Copy the code

  • Server.js part of the code

In this file, you can determine whether to implement renderToString method of SSR to construct Html string or downgrade to CSR to directly return SPA Html based on request URL parameters, ERR abnormal errors, and obtaining global configuration files.

Const app = express() const bundle = require('./dist/ vue-ssR-server-bundle. json' Vue-server-renderer /client-plugin generates the client to build the manifest object. This object contains information about the entire WebPack build process, allowing the Bundle renderer to automatically derive what needs to be injected into the HTML template. Const ssrTemplate = require('./dist/vue- SSR -client-manifest.json') const ssrTemplate = fs.readFileSync(resolve('./dist/index.ssr.html'), 'utf-8') const csrTemplate = fs.readFileSync(resolve('./dist/index.csr.html'), 'utF-8 ') // Call the createBundleRenderer method of vue-server-renderer to create the renderer and set the HTML template. Function createRenderer (bundle, options) {return createBundleRenderer(bundle, options) {return createBundleRenderer(bundle, options) Object.assign(options, { template: ssrTemplate, basedir: resolve('./dist'), runInNewContext: Let renderer = createRenderer(bundle, renderer); {clientManifest}) // Related middleware compression response file processing static resources etc app.use(...) // Set cache time const microCache = LRU({maxAge: 1000 * 60 * 1 }) function render (req, res) { const s = Date.now() res.setHeader('Content-Type', 'text/ HTML ') // cache hit code // Set the requested URL const context = {title: ", url: req.url,} if(/** with related URL parameters that need to be degraded to SSR, err error, get global configuration file... Condition */){res.end(csrTemplate) return} // Render the Vue instance as a string, passing in the context object. Renderer. renderToString(context, (err, HTML) => {if (err) {// Accidental error avoid throwing 500 errors can be degraded to CSR HTML file // play log operation..... Res.end (csrTemplate) return} res.end(HTML)})} // Start a service and listen on port 8080 app.get('*', render) const port = process.env.PORT || 8080 const server = http.createServer(app) server.listen(port, () => { console.log(`server started at localhost:${port}`) })Copy the code
  • entry.server.js
import { createApp } from './app' export default context => { return new Promise((resolve, reject) => { const { app, router, store } = createApp() const { url, req } = context const fullPath = router.resolve(url).route.fullPath if (fullPath ! == url) { return reject({ url: Router.push (URL) router.onReady(() => {const matchedComponents = router.getMatchedComponents() if (! matchedComponents.length) { reject({ code: All (matchedComponents. Map (({asyncData}) => asyncData && asyncData({store, route: router.currentRoute, req }))).then(() => { context.state = store.state if (router.currentRoute.meta) { context.title = router.currentRoute.meta.title } resolve(app) }).catch(reject) }, reject) }) }Copy the code

  • entry-client.js
Import 'es6-promise/auto' import {createApp} from './app' const {app, router, store} = createApp() Context.state is automatically embedded in the final HTML as the window.__initial_state__ state. On the client side, state is window.__initial_state__ before mounting to the application. if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } router.onReady(() => { router.beforeResolve((to, from, next) => { const matched = router.getMatchedComponents(to) const prevMatched = router.getMatchedComponents(from) let diffed = false const activated = matched.filter((c, i) => { return diffed || (diffed = prevMatched[i] ! == c) }) const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _) if (! asyncDataHooks.length) { return next() } Promise.all(asyncDataHooks.map(hook => hook({ store, route: To}))). Then (() = > {next ()}). The catch (next)}) / / mounted on the DOM app. $mount (' # app ')})Copy the code

6. Downgrade strategy

Node to carry out data persistence related work, so I/O and disk is the main bottleneck, Node as a front-end SSR service, CPU, memory, network is the main bottleneck. Rendering a full application based on Vue/React in Node.js is obviously cpu-intensive (cpu-intensive-CPU intensive) compared to a server that only serves static files.

6.1 Node server monitoring Is degraded

CPU indicators

Because processes block on the CPU for different reasons, for CPU-intensive tasks, CPU utilization is a good indicator of how well the CPU is working, but for I/O intensive tasks, idle CPU does not mean that the CPU is idle, but that the task has been suspended for other operations. For the SSR Node system, rendering can basically be understood as CPU intensive business, so this indicator can reflect the CPU performance of the current business environment to a certain extent.

Memory metrics

Memory is a very easy metric to quantify. Memory usage is a common indicator of a system’s memory bottleneck. In V8, all JavaScript objects are allocated via the heap. The definition of the value retrieved by process.memoryUsage() :

  • ‘heapTotal’ and ‘heapUsed’ represent V8 memory usage.
  • ‘external’ represents the memory usage of C++ objects bound to Javascript managed by V8.
  • RSS is the resident set size, how much physical memory (as a percentage of the total allocated memory) is allocated to the process, and contains all C++ and JavaScript objects and code. Use [` Worker `] (http://nodejs.cn/s/2X2C6P) thread, ` RSS ` would be a valid values for the whole process, and the other fields only points to the current thread.
  • ` arrayBuffers ` refers to assigned to ` ArrayBuffer ` and ` SharedArrayBuffer ` memory, including all of the Node. Js [` Buffer `] (http://nodejs.cn/s/qt7iHj). This is also contained in the ‘external’ value. When Node.js is used as an embedded library, this value may be ‘0’, as the allocation of ‘ArrayBuffer’ may not be tracked in this case.

The first thing to focus on is the memory stack, which is the footprint of the heap. In the single-threaded mode of Node, the C++ program (V8 engine) allocates Node memory as heapTotal for the Node thread. During Node use, new variables declared use this memory to store heapUsed. Node’s generational GC algorithm wastes memory resources to some extent, so global.gc() is forced when heapUsed reaches half of heapTotal. For system memory monitoring, it is not possible to just GC the system memory level as Node memory level, but also render degradation. 70% to 80% memory usage is a very dangerous situation. The specific value depends on the host where the environment is located.

const os = require('os') const sleep = ms => new Promise(resolve => setTimeout(resolve, @param {Number} options. ms default: 1000ms @param {Boolean} Options. The percentage of true (results returned as a percentage) | @ returns false (decimal return) * * / async {Promise} CPU utilization getCPULoadavg (Options = {}) { const that = this; const { cpuUsageMS = 1000, percentage = false } = options const t1 = that._getCPUMetric() await sleep(cpuUsageMS) const t2 = that._getCPUMetric() const idle = t2.idle - t1.idle const total = t2.total - t1.total let usage = 1 - idle / total percentage && (usage = ` ${(usage * 100.0) toFixed (2)} % `) return the usage} / get the memory information * * * * @ param {Boolean} percentage with true | (results returned as a percentage) False (decimal returns) * @returns {Object} Memory information */ getMemoryUsage(percentage = false) {const {RSS, heapUsed, heapTotal } = process.memoryUsage() const sysFree = os.freemem() const sysTotal = os.totalmem(); return { sys: percentage ? `${(1 - sysFree / sysTotal).toFixed(2) * 100}%` : 1 - sysFree / sysTotal, heap: percentage ? `${(heapUsed / heapTotal).toFixed(2) * 100}%` : heapUsed / heapTotal, node: percentage ? `${(rss / sysTotal).toFixed(2) * 100}%` : RSS/sysTotal}} /** * obtain CPU information * @returns {Object} CPU information */ _getCPUMetric() {const cpus = os.cpus(); let user = 0, nice = 0, sys = 0, idle = 0, irq = 0, total = 0 for (let cpu in cpus) { const times = cpus[cpu].times user += times.user nice += times.nice sys += times.sys idle += times.idle irq += times.irq } total += user + nice + sys + idle + irq return { user, sys, idle, total, } } } const osUtils = new OsUtils() osUtils.getCPULoadavg({ percentage: True}). Then (res => {console.log(' current CPU usage: ', res)}); Console. log(' Current memory information: ', osutils.getMemoryUsage (true))Copy the code

Set a threshold based on the CPU usage and memory specifications. If the threshold is exceeded, downgrade SSR to CSR.

6.2 Nginx configuration degraded

  1. In nginx configuration, SSR request is forwarded to Node rendering server and response status code interception is enabled.
  2. If the response is abnormal, change the abnormal status to 200 response and point to the new redirection rule.
  3. Redirection Rule Redirects the ADDRESS after removing the SSR directory and forwards the request to the static HTML file server.

The process is as follows:

Demotion to summarize

  • Occasional demotion – occasional server render failure demoted to client render
  • Configure platform degradation — modify the global configuration file by configuring the platform to actively degrade. For example, in the case of heavy traffic on Double 11, the entire application cluster can be degraded to client rendering by configuring the platform in advance;
  • Monitoring system degradation – Monitoring system running scheduled tasks to monitor cluster status. If cluster resource usage reaches the set CPU/ memory threshold, the entire cluster will be degraded or expanded.
  • Rendering service cluster down – SSR rendering can be understood as another form of BFF layer, the interface server is separate from SSR rendering server, and the HTML fetching logic goes back to Nginx fetching, which triggers client rendering.

reference

  • Environmental performance monitoring Node. Js – https://juejin.cn/post/6844903781889474567;
  • VueSSR higher-order guide – https://juejin.cn/post/6844903669922529287;
  • Vue server rendering (SSR) of actual combat – https://juejin.cn/post/6844903630147944455