基於 VUE-SSR 的性能優化

基於 VUE-SSR 的性能優化

關於 SSR(全稱 Server-side-render),每個前端同窗必定都很熟悉,咱們知道 SSR 能夠減小白屏等待時間,對 SEO 友好,容易被搜索引擎抓取到,可是咱們該怎麼寫好一個 SSR 項目呢?下面這篇文章由一道著名的面試題爲起點,帶你一步一步揭開 SSR 的奧祕。css

著名面試題:從瀏覽器中輸入 URL 發生了什麼。

這個過程簡單歸納爲幾大步:html

  1. DNS 解析
  2. TCP 鏈接
  3. 發送 HTTP 請求
  4. 服務器處理請求並返回 HTTP 報文
  5. 瀏覽器解析渲染頁面

做爲一個前端工程師咱們應該關注 三、四、5。前端

瀏覽器發送 HTTP 請求前,會首先檢查該資源是否存在緩存,有如下請求頭、響應頭做爲緩存標識:Expires、Cache-Control、Last-Modified、if-Modified-Since、Etag、if-None-Match,下面來給他們分個類。vue

緩存分爲強緩存、協商緩存兩種

強緩存

當瀏覽器準備發送 Http 請求請求一條資源時,它檢查以前曾經發過這條資源,並且這條資源當時的響應結果帶了 Expires 這個響應頭並設置了一個絕對的時間 Expires: Wed, 21 Oct 2021 00:00:00 GMT,這個時候瀏覽器一看,這條資源到 2021 年才過時呢,就不會發送請求了,而是直接取以前的返回結果。
Expires 是 http1.0 時代的強緩存依據,在 http1.1 又補充了 Cache-Control 這個響應頭做爲強緩存依據,Cache-Control 的一般用法是 Cache-Control: max-age=31600,它表示資源有效時間,是一個相對的時間。Cache-Control 的存在解決了當服務器時間和客戶端時間(瀏覽器的時間其實是依賴系統時間的,而咱們是可以隨意修改系統時間的)不一致引起的問題,咱們發出的 http 資源的強緩存依然有效,不會時間變長也不會變短。node

協商緩存

  • Last-Modified If-Modified-Since (http1.0)
    一個請求響應時會返回一個響應頭 Last-Modified 表示這個資源上次修改的時間,下次相同的資源請求會帶上 If-Modified-Since 這個請求頭,值和上次的 Last-Modified 相同,服務端經過判斷 If-Modified-Since 這個時間是不是當前資源的最後修改時間來決定是否使用緩存。若是可使用緩存就會以 304 狀態碼返回。
    這個方式有着和 Expires 響應頭相似的弊病:強依賴系統時間,當系統時間發生變化時,這個 Header 所帶來的緩存是否過時的信息將沒法被信賴。
  • Etag If-None-Match (http1.1)
    和上面的過程很是相似,服務端第一次返回資源時會生成一個資源的摘要,做爲響應頭 Etag 返回給客戶端。
    客戶端請求時經過 If-none-match 帶上以前 Etag 的值,服務端來比較 If-none-match 和當前資源的摘要是否一致來判斷是否命中緩存。

Webpack 與瀏覽器緩存的配合

經過瀏覽器緩存機制咱們能夠極大的減小瀏覽器請求的資源量,加快頁面的展示。在現代前端項目中,瀏覽器緩存機制每每是配合 Webpack 來實現的,咱們通常經過 Webpack 對項目進行打包。在 Webpack 中核心配置主要有 entry、output、module、plugin,經過如下最基礎的配置來對 Webpack 配置有一個基礎的印象。webpack

const path = require('path')
const { ProgressPlugin } = require('webpack')
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin-webpack4')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const handler = (percentage, message, ...args) => {
  console.info('構建進度:', percentage)
}
module.exports = {
  mode: 'production', // 可選值有 'node' || 'development' || 'production' production 會設置 process.env.NODE_ENV = 'production' 並開啓一些優化插件
  entry: './main.js', // Webpack 打包開始的入口文件
  output: {
    // 完成打包後的輸出規則
    path: path.resolve(__dirname, 'dist/'), // 輸出到當前目錄的 dist 目錄下
    filename: '[name].[hash].js' // 文件會按照 [name].[hash].js 的命名規則進行命名
  },

  /**
   * Webpack 只可以解析 Js 模塊,當遇到非 Js 的模塊、文件時,須要經過 loader 將其轉換成 Js
   */

  module: {
    rules: [
      { test: /\.vue$/, use: 'vue-loader' }, // 將 Vue 文件轉換爲 html、css、js 三部分
      {
        test: /\.(css|less)$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] //  Less => Css => Js => 最後利用 MiniCssExtractPlugin.loader 抽離出 css 文件。
      },
      {
        test: /\.js$/,
        use: ['babel-loader'] // 利用 babel 將 ES 高版本代碼編譯爲 ES5
      }
    ]
  },
  /**
   * Loader 的存在可以讓 Webpack 識別並轉換任何的代碼,可是缺乏在打包過程當中對資源進行操做的方式,Plugin 經過 Webpack 內置的鉤子函
   * 數給咱們提供了強大的擴展性,咱們能夠利用 Plugin 作不少事情。
   */

  plugins: [
    new CleanWebpackPlugin(), // 在打包前清空 output 的目標目錄
    new VueLoaderPlugin(), // 配合 Vue-loader 使用,將 Vue-loader 轉換出 Js 代碼從新根據 rules 配置的 loader 進行轉換
    new HtmlWebpackPlugin({
      // 利用指定的 html 模版在構建結束後生成一個 html 文件並引入構建後資源。
      template: 'index.html'
    }),
    new MiniCssExtractPlugin({
      // 將原本內置在 Js 的樣式代碼抽離成單獨的 css 文件
      filename: '[name].[hash].css',
      chunkFilename: '[id].[hash].css'
    }),
    new ProgressPlugin(handler) // 打印構建進度
  ]
}

經過以上 Webpack 配置構建後構建的結果以下所示
img
在構建結果中咱們可以看到,咱們的輸出的文件都按照根據本次構建的 hash 生成了一個文件名稱中帶有 hash 的文件,利用這個 hash 咱們可使用瀏覽器的強緩存,經過配置 Cache-Control: max-age={很大的數字}來是咱們的靜態資源可以保留在瀏覽器中,當下一次構建時會生成新的 hash,不會由於緩存而致使 Web 應用沒法更新。git

想要學習 Webpack,能夠看一看 Webpack 文檔,若是想要深刻的學習 編寫一個插件 是不可錯過的。

利用 CDN 對靜態資源進行加速

CDN 全稱是 Content Delivery Network(內容分發網絡),它的做用是減小傳播時延,找最近的節點。經過以上緩存的方式咱們解決了重複請求資源的效率問題,可是當第一次請求資源時,這好幾 Mb 的內容夠用戶加載好一下子了,若是都是從服務器中發出,可想而知服務器的貸款壓力有多大。
CDN 的存在幫咱們解決了這個問題,CDN 的主要做用就是減輕源站(服務器)負載,經過部署在全球各地的節點返回數據。真正的 CDN 可能在某個地區的運營商都會有一個專門的節點。
imggithub

咱們將內容上傳至 CDN 源站中,當第一次訪問該資源的時候會進行 DNS 查詢得到該域名的 CNAME 記錄,而後對新的 CNAME 進行 DNS 查詢會獲得一個離用戶訪問最近的邊緣服務器的 IP 地址,用戶瀏覽器與邊緣服務器創建 TCP 連接,將 HTTP 請求發送到邊緣服務器,邊緣服務器檢查是否有該資源,若是沒有該資源會進行回源,向上一級 CDN 服務器請求該資源,直至找到該資源並返回給邊緣服務器,邊緣服務器會緩存該資源,並返回給用戶。當另外一個用戶訪問到同一個邊緣服務器時,就能很快的獲取該資源。web

Vue-Spa 使首屏加載變慢

在解釋爲什麼 Vue-Spa 使首屏加載變慢前咱們首先須要瞭解當瀏覽器請求到資源後是如何渲染資源的。面試

  1. 瀏覽器請求到 HTML 文檔並構建文檔對象模型(DOM)
  2. 瀏覽器加載樣式文件,構建層疊樣式表模型(CSSOM)
  3. 在 DOM 和 CSSOM 構建後會生成渲染樹,包括一切將要被渲染的對象,除了<head> display:none等不可見的標籤
  4. 根據渲染樹會進行 layout 過程,肯定每一個元素的位置、大小等,並最終調用操做系統繪製在顯示屏幕上。

若是咱們直接請求一個 html 文件就是上面的過程,這個過程很是快,在幾毫秒就能夠完成。

可是得力與前端技術的發展,咱們開發的大型 WEB 應用沒法經過一個 Html 就能傳給用戶使用,咱們在 Html 中引入了不少不少 Javascript 文件,並經過 Javascript 來渲染咱們的應用。以 Vue-Spa 爲例咱們從新講解這個渲染過程。

  1. 瀏覽器請求到 HTML 文檔並構建文檔對象模型(DOM)
  2. 瀏覽器加載樣式文件,構建層疊樣式表模型(CSSOM)
  3. 在 DOM 和 CSSOM 構建後會生成渲染樹,包括一切將要被渲染的對象,除了<head> display:none等不可見的標籤
  4. 根據渲染樹會進行 layout 過程,肯定每一個元素的位置、大小等,並最終調用操做系統 API 繪製在顯示屏幕上。

瀏覽器依然會走以上這四步過程,可是由於咱們的 html 中除了一些 <script> <link>之外幾乎是空的,因此是白屏狀態。

  1. 瀏覽器並行下載 script link 資源(若是首次訪問,這個過程很是長,在網絡環境很差的時候甚至會長達十幾秒)
  2. 瀏覽器啓動 V8 引擎執行咱們的 Js 代碼,以 Vue 爲例:

    • 6.1 建立 Vue 實例,經過 Vue 中的 Observer 將 Vue 實例中的 data 變成可響應的,利用 Dep 進行依賴收集,爲每個 Vue 實例生成一個 Render-watcher 好讓數據變化時可以通知 Vue 進行渲染。
    • 6.2 渲染過程會首先生成 Virtual-Dom,而後經過時間複雜度爲 O(n) 的對比算法和老的 Old Virtual-Dom 進行對比,並同時調用平臺(瀏覽器)的渲染 Api 進行 打補丁(Patch)
  3. 瀏覽器構建新的 DOM 和 CSSOM,並修改 render-tree 進行迴流,最終會根據新的元素位置、大小調用操做系統 API 繪製在顯示屏幕上。

能夠看到 Vue-Spa 比直接渲染 Html 的方式多出了 五、六、7 步驟,而且多出了幾倍的耗時。

因此就有了骨架屏的優化思路,在第一次返回的 Html 中不反悔白屏的空內容了,而是返回一個骨架屏或者 Loading 的圖標,提示用戶耐心等待,但這不是用戶想要看到的,用戶但願看到內容

Vue-SSR 如何優化首屏加載

Vue.js 是構建客戶端應用程序的框架。默認狀況下,能夠在瀏覽器中輸出 Vue 組件,進行生成 DOM 和操做 DOM。然而,也能夠將同一個組件渲染爲服務器端的 HTML 字符串,將它們直接發送到瀏覽器,最後將這些靜態標記"激活"爲客戶端上徹底可交互的應用程序。這樣咱們首屏就可以看到一部份內容,而不是空白、或者 loading 提示了。

最簡單的實現

Vue-ssr 經過 createRender() 方法生成一個 renderer 實例,利用 renderer 對象咱們能夠將 vue 實例轉換爲 html。

const app = new Vue({
  template: `<div>Hello World</div>`
})

const renderer = require('vue-server-renderer').createRenderer()

renderer.renderToString(app, (err, html) => {})

面向生產環境 SSR 還須要解決哪些問題

咱們依然須要知足一套代碼能在 Server 端和瀏覽器端同時運行,官方給出了以下的流程圖。

根據上圖,咱們能夠看到,咱們編寫通用的 Web 代碼,使用 Webpack 經過 entry-serverentry-client 兩個入口打包出 server-bundleClient-bundle,服務端使用 server-bundle渲染出的 Html 與 client-bundle 進行混合最終共同運行在瀏覽器上。

在生產環境中咱們不會調用 createRenderer 這個方法來進行服務端渲染,由於 Server 端的代碼會依賴 Client 端代碼,使得 Server 端會隨着 Client 端的代碼更新頻繁重啓。在生產環境中咱們使用 createBundleRenderer 來進行服務端渲染,也就是上圖所用的流程。

第一步、編寫通用的 app.js

用戶與客戶端的關係是一對一,而與服務器的關係是多對一,因此不能像 Spa 那樣使用一個單例的 Vue 實例,會形成不一樣用戶之間的數據共享,咱們首先要將以前的單例模式更改成工廠函數,動態生成Vue Vue-router Vuex 實例。

import Vue from 'vue'
import App from './App.vue'
import { createRouter } from './router' // 背後是 new Router()
import { createStore } from './store' // 背後是 new Vuex.store()

export const createApp = () => {
  const router = createRouter()
  const store = createStore()
  const app = new Vue({
    router,
    store,
    render: h => h(App)
  })
  return { app, router, store }
}

第二步、編寫 entry-server、entry-client 兩個入口文件

兩個入口文件對應打包出得 bundle 文件分別執行不一樣的職責:

  • 服務器端:僅在服務器端執行,將 Vue 實例渲染爲 html 字符串,注入到模板頁的對應位置中
  • 客戶端:僅在瀏覽器端執行,向模板頁中注入 js、css 等靜態資源
    具體解釋看代碼註釋。
// entry-server.js
import { createApp } from './app'
export default context => {
  // 由於有可能會是異步路由鉤子函數或組件,因此咱們將返回一個 Promise,
  // 以便服務器可以等待全部的內容在渲染前,
  // 就已經準備就緒。
  return new Promise((resolve, reject) => {
    const { app, router, store } = createApp() // 建立新的 Vue、Vue-router、Vuex 實例
    const { url } = context // context 是 Server 端傳過來請求上下文,咱們經過這個對象取出請求的 url
    router.push(url).catch(err => {
      // 將服務端的 Vue-router 的路徑修改成 url
      reject(err)
    })

    // 等到 router 將可能的異步組件和鉤子函數解析完
    router.onReady(() => {
      const matchedComponents = router.getMatchedComponents() // 獲取當前路由匹配到的 Vue 組件實例
      if (!matchedComponents.length) {
        // 沒有匹配到則拋出錯誤
        return reject({ code: 404 })
      }

      Promise.all(
        matchedComponents.map(
          // 運行匹配組件的 asyncData 鉤子函數進行數據預取,並將預取的數據放在 Vuex 中。
          ({ asyncData }) =>
            asyncData &&
            asyncData({
              store,
              route: router.currentRoute
            })
        )
      )
        .then(() => {
          context.state = store.state // 將 vuex 的 state 賦值給 context.state ,最終將自動序列化爲 window.__INITIAL_STATE__,並注入 HTML。
          resolve(app) // 返回 數據預取後的 Vue 實例
        })
        .catch(reject)
    })
  })
}
// entry-client.js
import { createApp } from './app'
const { app, router, store } = createApp() // 建立新的 Vue、Vue-router、Vuex 實例

if (window.__INITIAL_STATE__) {
  // 將服務端預取的數據賦值給客戶端的 vuex。
  store.replaceState(window.__INITIAL_STATE__)
}
router.onReady(() => {
  // 添加路由鉤子函數,用於處理 asyncData.
  // 在初始路由 resolve 後執行,
  // 以便咱們不會二次預取(double-fetch)已有的數據。
  // 使用 `router.beforeResolve()`,以便確保全部異步組件都 resolve。
  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)
    })

    // 由於此時客戶端 bundle 接管服務端渲染的 html,已經變成了一個單頁應用,咱們能夠在代碼中進行 router.push 來實現虛擬路由跳轉,可是代碼中不會執行 asyncData 數據預取這部分邏輯,因此這裏咱們要將新的組件中的 asyncData 拿出來執行。
    const asyncDataHooks = activated.map(c => c.asyncData).filter(_ => _)
    if (!asyncDataHooks.length) {
      return next()
    }
    Promise.all(asyncDataHooks.map(hook => hook({ store, route: to })))
      .then(() => {
        next()
      })
      .catch(next)
  })

  // 服務端渲染出得 html 在瀏覽器渲染後,會有一個 `data-server-rendered="true"` 的標記,標明這部分 Dom 是服務端渲染的,瀏覽器端的代碼準備好後就會接管這部分 Dom,使其從新變爲一個單頁應用。
  app.$mount('#container')
})

第三步、打包環境配置

咱們須要在配置文件中生成兩份配置文件分別爲 webpack.server.conf.js webpack.client.conf.js

const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

// webpack.server.conf.js 主要是和客戶端構建不一樣的地方
module.exports = {
  // 這容許 webpack 以 Node 適用方式(Node-appropriate fashion)處理動態導入(dynamic import),而且還會在編譯 Vue 組件時,告知 `vue-loader` 輸送面向服務器代碼(server-oriented code)。
  target: 'node',

  // 入口文件爲 entry-server.js
  entry: path.resolve(__dirname, '../code/client/src/entry-server.js'),

  // 此處告知 server bundle 使用 Node 風格導出模塊(Node-style exports)
  output: {
    filename: 'server-bundle.js',
    libraryTarget: 'commonjs2'
  },

  // 由於 Node 能夠依賴 node_modules 運行,因此不須要打包 node_modules 中的依賴,外置化應用程序依賴模塊,可使服務器構建速度更快,並生成較小的 bundle 文件。
  externals: nodeExternals({
    // 不要外置化 webpack 須要處理的依賴模塊。你能夠在這裏添加更多的文件類型。例如,未處理 *.css 文件,
    whitelist: /\.css$/
  }),
  plugins: [
    // 這是將服務器的整個輸出,構建爲單個 JSON 文件的插件,默認文件名爲 `vue-ssr-server-bundle.json`
    new VueSSRServerPlugin()
  ]
}
// webpack.client.conf.js
const VueSSRClientPlugin = require('vue-server-renderer/client-plugin')

module.exports = {
  entry: {
    app: path.resolve(__dirname, '../code/client/src/entry-client.js')
  },
  plugins: [
    // 這是將客戶端的整個輸出,構建爲單個 JSON 文件的插件,默認文件名爲 `vue-ssr-client-manifest.json`
    new VueSSRClientPlugin()
  ]
}

經過 Vue 官方提供的 vue-server-render/server-plugin vue-server-render/client-plugin 兩個插件咱們在構建完成後生成了 vue-ssr-server-bundle.json vue-ssr-client-manifest.json

  • vue-ssr-server-bundle.json 裏的內容爲:
// 這裏的entry和files參數是vue-ssr-server-bundle.json中的entry和files字段,分別是應用的入口文件名和打包的文件內容集合。
{
  "entry": "server-bundle.js",
  "files": {
    "server-bundle.js": "module.exports=xxx..."
  }
}
  • vue-ssr-client-manifest.json 裏的內容爲:
{
  "publicPath": "/client/",
  "all": [                              // 客戶端打包生成的所有資源文件
    "index.html",
    "static/js/app.7825d6691cb956e176c7.js",
    "static/js/manifest.ec516eefca3b4e60fa2e.min.js",
    "static/js/vendor.5c495484f630d50d4de0.js"
  ],
  "initial": [                          // 會以 preload 的形式插入到服務端生成的 html 中的資源文件
    "static/js/manifest.ec516eefca3b4e60fa2e.min.js",
    "static/js/vendor.5c495484f630d50d4de0.js",
  ],
  "async": [                            // 會以 prefetch 的形式插入到服務端生成的 html 中的資源文件
     "static/js/app.7825d6691cb956e176c7.js"
  ],
  "modules": {      // 項目的各個模塊包含的文件的序號,對應all中文件的順序
    "25965440": [
      3
    ],
    ...
  }
}

咱們會在這裏調用這兩個文件,來生成服務端渲染的 html。

const { createBundleRenderer } = require('vue-server-renderer')
const serverBundle = require('path-to-vue-ssr-server-bundle.json/vue-ssr-server-bundle.json')
const clientManifest = require('path-to-vue-ssr-client-manifest.json/vue-ssr-client-manifest.json')
const renderer = createBundleRenderer(serverBundle, {
  clientManifest
})

第四步、起一個 Node 服務

監聽 Http 請求並調用 renderer.renderToString 生成 html 返回給客戶端

const Koa = require('koa')
const koaRouter = require('koa-router')
const { createBundleRenderer } = require('vue-server-renderer')
const serverBundle = require('path-to-vue-ssr-server-bundle.json/vue-ssr-server-bundle.json')
const clientManifest = require('path-to-vue-ssr-client-manifest.json/vue-ssr-client-manifest.json')

const app = new Koa()
const router = koaRouter()

const renderer = createBundleRenderer(serverBundle, {  // 利用 serverBundle 和 clientManifest 生成 renderer
  clientManifest
})

const renderData = function(context) {
  // 包裝 renderToString 方法
  return new Promise((resolve, reject) => {
    renderer.renderToString(context, (err, html) => {
      if (err) {
        return reject(err)
      }
      resolve(html)
    })
  })
}

router.get('*', async (ctx, next) => {
  let html
  try {
    html = await renderData(ctx)
  } catch (e) {
    if (e.code === 404) {  // 處理渲染的異常狀況
      status = 404
      html = '404 | Not Found'
    } else {
      status = 500
      html = '500 | Internal Server Error'
    }
  }
  ctx.body = html // 返回構建的 html
})

app.use(router.routes()).use(router.allowedMethods())

app.listen(3000)

一些其它的細節問題

在服務端渲染中,Vue 實例的生命週期只會執行 beforeCreate created 兩個生命週期,在這兩個生命週期要注意區分是在 server 環境仍是在 瀏覽器環境,會佔用全局內存的邏輯,如定時器、全局變量、閉包等,儘可能不要放在 beforeCreate、created 鉤子中,不然在 beforeDestory 方法中將沒法註銷,致使內存泄漏。

SSR 性能優化

SSR 項目比 SPA 項目要佔用更多的服務器資源用於數據預取html 渲染,比較耗費 CPU 資源和網絡資源,

一、開啓 Node 多進程

Node.js 雖然是單線程模型,可是其基於事件驅動、異步非阻塞模式,能夠應用於高併發場景,避免了線程建立、線程之間上下文切換所產生的資源開銷。可是卻遇到大量計算,CPU 耗時的操做,則沒法經過開啓線程利用 CPU 多核資源,可是能夠經過開啓進程的方式,來利用服務器的多核資源。

  • 經過 cluster 模塊開啓多進程
    cluster 管理多進程的方式爲 主-從 模式,master 進程負責開啓、調度 worker 進程,worker 進程負責處理請求和其餘實際的 server 邏輯。
const cluster = require('cluster')
const http = require('http')
let cupsLength = require('os').cpus().length

if (cluster.isMaster) {
  while (cupsLength--) {
    cluster.fork() // 複製出其餘的 worker 進程
  }
} else {
  // 執行端口監聽的邏輯。
}
  • 經過 pm2 開啓多進程
    pm2 也是使用了 Node.js 的 cluster 來作進程管理,但對於開發者來講更加友好,咱們能夠經過 pm2 清晰地看見整個集羣的模式、狀態,CPU 利用率甚至是內存大小。
pm2 start index.js -i max

二、開啓緩存

緩存能夠利用 vue-ssr 提供的頁面級緩存和組件緩存兩種

  • 頁面級緩存:在建立 render 實例時利用最近最少使用算法來緩存當前請求的資源。
const LRU = require('lru-cache')

const renderer = createRenderer({
  cache: LRU({
    max: 10000,
    maxAge: ...
  })
})
  • 組件級緩存:可緩存組件還必須定義一個惟一的 name 選項。經過使用惟一的名稱,每一個緩存鍵 (cache key) 對應一個組件。若是 renderer 在組件渲染過程當中進行緩存命中,那麼它將直接從新使用整個子樹的緩存結果。
export default {
  name: 'item', // 必填選項
  props: ['item'],
  serverCacheKey: props => props.item.id,
  render(h) {
    return h('div', this.item.id)
  }
}s
  • 利用 Redis 進行緩存:當咱們將 SSR 應用程序部署在多服務、多進程下時,以上緩存方式的效果就大打折扣,由於每次請求被一個進程處理時,該進程可能並無緩存過這條資源,可是其餘的進程卻緩存過屢次,這樣會致使緩存命中大打折扣,咱們能夠起一個 Redis 服務,專門用來存儲須要緩存的資源。如下是利用 Redis 時首次渲染和第二次命中緩存渲染的時間:
  • 首次加載由於緩存中沒有數據,便會進行接口請求、數據庫查詢進行數據預取 耗時 263 ms,渲染耗時 10 ms。
  • 第二次加載命中 redis 緩存再也不須要數據預取和渲染,只花了與 redis 數據庫通訊的時間 2ms。

三、 降級處理

當咱們遇到大量的請求時,服務器壓力過大或渲染出錯時咱們須要拋棄服務端渲染該用客戶端渲染。

  • 單次渲染降級

當某次請求的服務端渲染出錯時,中止服務端渲染,並將 SPA 應用的 HTML 模板返回給用戶。

  • Cpu 壓力過大降級

能夠經過 Node 的 os.loadavg() 來獲取最近 1 分鐘的 cpu 佔用率,當發現 Cpu 使用率太高時能夠降級爲 Spa 應用。

最後,雙手奉上開箱即用的 Demo 吧: Vue-ssr 模版
相關文章
相關標籤/搜索