Vue服务端渲染和Vue浏览器端渲染的性能对比

Vue 2.0 开始支持服务端渲染的功能,因此本文章也是基于vue 2.0以上版本。网上对于服务端渲染的资料仍是比较少,最经典的莫过于Vue做者尤雨溪大神的 vue-hacker-news。本人在公司作Vue项目的时候,一直苦于产品、客户对首屏加载要求,SEO的诉求,也想过不少解决方案,本次也是针对浏览器渲染不足之处,采用了服务端渲染,而且作了两个同样的Demo做为比较,更能直观的对比Vue先后端的渲染。javascript

talk is cheap,show us the code!话很少说,咱们分别来看两个Demo:(欢迎star 欢迎pull request)html

1.浏览器端渲染Demo: https://github.com/monkeyWangs/doubanMovievue

2.服务端渲染Demo:https://github.com/monkeyWangs/doubanMovie-SSRjava

两套代码运行结果都是为了展现豆瓣电影的,运行效果也都是差很少,下面咱们来分别简单的阐述一下项目的机理:node

1、浏览器端渲染豆瓣电影

首先咱们用官网的脚手架搭建起来一个vue项目webpack

npm install -g vue-cli
vue init webpack doubanMovie
cd doubanMovie
npm install
npm run dev

  

这样即可以简单地打起来一个cli框架,下面咱们要作的事情就是分别配置 vue-router, vuex,而后配置咱们的webpack proxyTable 让他支持代理访问豆瓣API。git

1.配置Vue-routeres6

咱们须要三个导航页:正在上映、即将上映、Top250;一个详情页,一个搜索页。这里我给他们分别配置了各自的路由。在 router/index.js 下配置如下信息:github

import Vue from 'vue'
import Router from 'vue-router'
import Moving from '@/components/moving'
import Upcoming from '@/components/upcoming'
import Top250 from '@/components/top250'
import MoviesDetail from '@/components/common/moviesDetail'

import Search from '@/components/searchList'

Vue.use(Router)
/**
 * 路由信息配置
 */
export default new Router({
  routes: [
    {
      path: '/',
      name: 'Moving',
      component: Moving
    },
    {
      path: '/upcoming',
      name: 'upcoming',
      component: Upcoming
    },
    {
      path: '/top250',
      name: 'Top250',
      component: Top250
    },
    {
      path: '/search',
      name: 'Search',
      component: Search
    },
    {
      path: '/moviesDetail',
      name: 'moviesDetail',
      component: MoviesDetail
    }

  ]
})

  

这样咱们的路由信息配置好了,而后每次切换路由的时候,尽可能避免不要重复请求数据,因此咱们还须要配置一下组件的keep-alive:在app.vue组件里面。web

<keep-alive exclude="moviesDetail">
   <router-view></router-view>
</keep-alive>

 

这样一个基本的vue-router就配置好了。

2.引入vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的全部组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

简而言之:Vuex 至关于某种意义上设置了读写权限的全局变量,将数据保存保存到该“全局变量”下,并经过必定的方法去读写数据。

Vuex 并不限制你的代码结构。可是,它规定了一些须要遵照的规则:

  1. 应用层级的状态应该集中到单个 store 对象中。

  2. 提交 mutation 是更改状态的惟一方法,而且这个过程是同步的。

  3. 异步逻辑都应该封装到 action 里面。

对于大型应用咱们会但愿把 Vuex 相关代码分割到模块中。下面是项目结构示例:

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 咱们组装模块并导出 store 的地方
    └── moving            # 电影模块
        ├── index.js      # 模块内组装,并导出模块的地方
        ├── actions.js    # 模块基本 action
        ├── getters.js    # 模块级别 getters
        ├── mutations.js  # 模块级别 mutations
        └── types.js      # 模块级别 types

  

因此咱们开始在咱们的src目录下新建一个名为store 的文件夹 为了后期考虑 咱们新建了moving 文件夹,用来组织电影,考虑到全部的action,getters,mutations,都写在一块儿,文件太混乱,因此我又给他们分别提取出来。

stroe文件夹建好,咱们要开始在main.js里面引用vuex实例:

import store from './store'
new Vue({
  el: '#app', router, store, template: '<App/>', components: { App } })

 

这样,咱们即可以在全部的子组件里经过 this.$store 来使用vuex了。

3.webpack proxyTable 代理跨域

webpack 开发环境可使用proxyTable 来代理跨域,生产环境的话能够根据各自的服务器进行配置代理跨域就好了。在咱们的项目config/index.js 文件下能够看到有一个proxyTable的属性,咱们对其简单的改写

proxyTable: {
      '/api': { target: 'http://api.douban.com/v2', changeOrigin: true, pathRewrite: { '^/api': '' } } }

 

这样当咱们访问

localhost:8080/api/movie

 

的时候 其实咱们访问的是

http://api.douban.com/v2/movie

 

这样便达到了一种跨域请求的方案。

至此,浏览器端的主要配置已经介绍完了,下面咱们来看看运行的结果:

为了介绍浏览器渲染是怎么回事,咱们运行一下npm run build 看看咱们的发布版本的文件,究竟是什么鬼东西....

run build 后会都出一个dist目录 ,咱们能够看到里面有个index.html,这个即是咱们最终页面将要展现的html,咱们打开,能够看到下面:

观察好的小伙伴能够发现,咱们并无多余的dom元素,就只有一个div,那么页面要怎么呈现呢?答案是js append,对,下面的那些js会负责innerHTML。而js是由浏览器解释执行的,因此呢,咱们称之为浏览器渲染,这有几个致命的缺点:

  1. js放在dom结尾,若是js文件过大,那么必然形成页面阻塞。用户体验明显很差(这也是我我在公司反复被产品逼问的事情)
  2. 不利于SEO
  3. 客户端运行在老的JavaScript引擎上

对于世界上的一些地区人,可能只能用1998年产的电脑访问互联网的方式使用计算机。而Vue只能运行在IE9以上的浏览器,你可能也想为那些老式浏览器提供基础内容 - 或者是在命令行中使用 Lynx的时髦的黑客

基于以上的一些问题,服务端渲染呼之欲出....

2、服务器端渲染豆瓣电影

先看一张Vue官网的服务端渲染示意图

从图上能够看出,ssr 有两个入口文件,client.js 和 server.js, 都包含了应用代码,webpack 经过两个入口文件分别打包成给服务端用的 server bundle 和给客户端用的 client bundle. 当服务器接收到了来自客户端的请求以后,会建立一个渲染器 bundleRenderer,这个 bundleRenderer 会读取上面生成的 server bundle 文件,而且执行它的代码, 而后发送一个生成好的 html 到浏览器,等到客户端加载了 client bundle 以后,会和服务端生成的DOM 进行 Hydration(判断这个DOM 和本身即将生成的DOM 是否相同,若是相同就将客户端的vue实例挂载到这个DOM上, 不然会提示警告)。

具体实现:

咱们须要vuex,须要router,须要服务器,须要服务缓存,须要代理跨域....不急咱们慢慢来。

1.创建nodejs服务

首先咱们须要一个服务器,那么对于nodejs,express是很好地选择。咱们来创建一个server.js

const port = process.env.PORT || 8080
app.listen(port, () => { console.log(`server started at localhost:${port}`) })

 

这里用来启动服务监听 8080 端口。

而后咱们开始处理全部的get请求,当请求页面的时候,咱们须要渲染页面

app.get('*', (req, res) => {
  if (!renderer) { return res.end('waiting for compilation... refresh in a moment.') } const s = Date.now() res.setHeader("Content-Type", "text/html") res.setHeader("Server", serverInfo) const errorHandler = err => { if (err && err.code === 404) { res.status(404).end('404 | Page Not Found') } else { // Render Error Page or Redirect res.status(500).end('500 | Internal Server Error') console.error(`error during render : ${req.url}`) console.error(err) } } renderer.renderToStream({ url: req.url }) .on('error', errorHandler) .on('end', () => console.log(`whole request: ${Date.now() - s}ms`)) .pipe(res) })

 

而后咱们须要代理请求,这样才能进行跨域,咱们引入http-proxy-middleware模块:

const proxy = require('http-proxy-middleware');//引入代理中间件
/**
 * proxy middleware options
 * 代理跨域配置
 * @type {{target: string, changeOrigin: boolean, pathRewrite: {^/api: string}}}
 */
var options = {
  target: 'http://api.douban.com/v2', // target host
  changeOrigin: true,               // needed for virtual hosted sites
 pathRewrite: { '^/api': '' } }; var exampleProxy = proxy(options); app.use('/api', exampleProxy);

 

这样咱们的服务端server.js便配置完成。接下来 咱们须要配置服务端入口文件,还有客户端入口文件,首先来配置一下客户端文件,新建src/entry-client.js

import 'es6-promise/auto'
import { app, store, router } from './app'

// prime the store with server-initialized state.
// the state is determined during SSR and inlined in the page markup.
if (window.__INITIAL_STATE__) { store.replaceState(window.__INITIAL_STATE__) } /** * 异步组件 */ router.onReady(() => { // 开始挂载到dom上 app.$mount('#app') }) // service worker if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { navigator.serviceWorker.register('/service-worker.js') }

 

客户端入口文件很简单,同步服务端发送过来的数据,而后把 vue 实例挂载到服务端渲染的 DOM 上。

再配置一下服务端入口文件:src/entry-server.js

import { app, router, store } from './app'

const isDev = process.env.NODE_ENV !== 'production'

// This exported function will be called by `bundleRenderer`.
// This is where we perform data-prefetching to determine the
// state of our application before actually rendering it.
// Since data fetching is async, this function is expected to
// return a Promise that resolves to the app instance.
export default context => { const s = isDev && Date.now() return new Promise((resolve, reject) => { // set router's location  router.push(context.url) // wait until router has resolved possible async hooks router.onReady(() => { const matchedComponents = router.getMatchedComponents() // no matched routes if (!matchedComponents.length) { reject({ code: 404 }) } // Call preFetch hooks on components matched by the route. // A preFetch hook dispatches a store action and returns a Promise, // which is resolved when the action is complete and store state has been // updated. Promise.all(matchedComponents.map(component => { return component.preFetch && component.preFetch(store) })).then(() => { isDev && console.log(`data pre-fetch: ${Date.now() - s}ms`) // After all preFetch hooks are resolved, our store is now // filled with the state needed to render the app. // Expose the state on the render context, and let the request handler // inline the state in the HTML response. This allows the client-side // store to pick-up the server-side state without having to duplicate // the initial data fetching on the client. context.state = store.state resolve(app) }).catch(reject) }) }) }

 

server.js 返回一个函数,该函数接受一个从服务端传递过来的 context 的参数,将 vue 实例经过 promise 返回。context 通常包含 当前页面的url,首先咱们调用 vue-router 的 router.push(url) 切换到到对应的路由, 而后调用 getMatchedComponents 方法返回对应要渲染的组件, 这里会检查组件是否有 fetchServerData 方法,若是有就会执行它。

下面这行代码将服务端获取到的数据挂载到 context 对象上,后面会把这些数据直接发送到浏览器端与客户端的vue 实例进行数据(状态)同步。

context.state = store.state

 

而后咱们分别配置客户端和服务端webpack,这里能够在个人github上fork下来参考配置,里面每一步都有注释,这里再也不赘述。

接着咱们须要建立app.js:

import Vue from 'vue'
import App from './App.vue' import store from './store' import router from './router' import { sync } from 'vuex-router-sync' import Element from 'element-ui' Vue.use(Element) // sync the router with the vuex store. // this registers `store.state.route` sync(store, router) /** * 建立vue实例 * 在这里注入 router store 到全部的子组件 * 这样就能够在任何地方使用 `this.$router` and `this.$store` * @type {Vue$2} */ const app = new Vue({ router, store, render: h => h(App) }) /** * 导出 router and store. * 在这里不须要挂载到app上。这里和浏览器渲染不同 */ export { app, router, store }

 

这样 服务端入口文件和客户端入口文件便有了一个公共实例Vue, 和咱们之前写的vue实例差异不大,可是咱们不会在这里将app mount到DOM上,由于这个实例也会在服务端去运行,这里直接将 app 暴露出去。

接下来建立路由router,建立vuex跟客户端都差很少。详细的能够参考个人项目...

到此,服务端渲染配置 就简单介绍完了,下面咱们启动项目简单的看下:

这里跟服务端界面同样,不同的是url已经不是以前的 #/而变成了请求形式 /

这样每当浏览器发送一个页面的请求,会有服务器渲染出一个dom字符串返回,直接在浏览器段显示,这样就避免了浏览器端渲染的不少问题。

提及SSR,其实早在SPA (Single Page Application) 出现以前,网页就是在服务端渲染的。服务器接收到客户端请求后,将数据和模板拼接成完整的页面响应到客户端。 客户端直接渲染, 此时用户但愿浏览新的页面,就必须重复这个过程, 刷新页面. 这种体验在Web技术发展的当下是几乎不能被接受的,因而愈来愈多的技术方案涌现,力求 实现无页面刷新或者局部刷新来达到优秀的交互体验。可是SEO倒是致命的,因此一切看应用场景,这里只为你们提供技术思路,为vue开发提供多一种可能的方案。

为了更清晰的对比两次渲染的结果,我作了一次实验,把两个想的项目build后模拟生产环境,在浏览器netWork模拟网速3g环境,先来看看服务端渲染的结果:

能够看到总体加载dom一共花了832ms;用户可能在网络比较慢的状况下从远处访问网站 - 或者经过比较差的带宽。 这些状况下,尽可能减小页面请求数量,来保证用户尽快看到基本的内容。

接下来咱们再看看浏览器端渲染的结果:

咱们能够看到其中有一个vendor.js 达到了563KB,总体的加载时间达到了了8.19s,这是由于单页面文件的缘由,会把全部的逻辑代码打包到一个js里面。能够用webpack拆分代码避免强制用户下载整个单页面应用,可是,这样也远没有下载个单独的预先渲染过的HTML文件性能高。

想要本身运行对比的小伙伴能够访问个人github,我已将源码放到上面:

1.浏览器端渲染Demo: https://github.com/monkeyWangs/doubanMovie

2.服务端渲染Demo:https://github.com/monkeyWangs/doubanMovie-SSR