前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >vue 随记(5):性能的飞跃

vue 随记(5):性能的飞跃

作者头像
一粒小麦
发布2020-07-23 15:31:28
1.3K0
发布2020-07-23 15:31:28
举报
文章被收录于专栏:一Li小麦
性能的飞跃

1. compile

尤雨溪的B站直播介绍到更新相比于vue2有1.3~2倍的性能优势。那么vue3比vue2块在哪里?

•Proxy取代defineProperty。这个之前的文章已经提过了。•虚拟dom(v-dom)重写--->静态标记:主要体现在纯粹静态节点将被标记•diff算法:vue2是双端比较。vue3加入了最长递增子序列(一种算法)。

1.1 vue3的模板是html吗?

或许这个网址能给你一点启示:http://vue-next-template-explorer.netlify.app/。

当我在模板写下这段代码:

代码语言:javascript
复制
<div>
  <div>djtao</div>
  <div>{{age}}</div>
</div>

看似html的代码经过vue 3编译,其实是一段js。

代码语言:javascript
复制
import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("div", null, "djtao"),
    _createVNode("div", null, _toDisplayString(_ctx.age), 1 /* TEXT */)
  ]))
}

留意到模板代码中存在变量的时候,_createVNode方法多了第四个参数1,提示node为文本节点。而模板中的djtao作为纯静态节点,第四个参数不传,就是纯静态节点,在vdom diff的时候,会被直接忽略。

我们再从模板中加一段:

代码语言:javascript
复制
<div :id="aaa">aaa</div>
代码语言:javascript
复制
// 编译后
_createVNode("div", { id: _ctx.aaa }, "aaa", 8 /* PROPS */, ["id"])

节点的动态部分,会维护在一个数组里。

vue3通过_createVNode方法的第四个参数,可以确定哪些是动态的,diff的时候判断是需要操作text,属性亦或是class。上面的例子中,第四个参数为1表示只需要关心text。第四个参数为8,表示只需要关心节点的id。

想阅读相关代码,可以在源码package/src/shared/src/patchFlags.ts中找到。

1.2 compile的本质

编译就是把看起来像html的模板字符串,转化为js的过程。

在jquery时代,原本就没有“模板字符串”这种说法。JS想要生成html都是非常暴力的html()操作。到了js库underscore问世之后,就发明了一种奇怪的写法:

<%= 标记变量•<% 标记js语法

于是你可能从那个时代看到了这种前端代码:

代码语言:javascript
复制
<script type="text/template" id="tpl">
<% _.each(data, function (item) { %>
  <div class="outer">
    <%= item.title %> - <%= item.url %> - <%= item.film %>
  </div>
<% }); %>
</script>

框架通过解析这段字符串,判断哪些是变量,那些是html节点,并通过innerHTML来生成html代码。

underscore的模板可以说是一种进步,因为前端可以在相对直观的视野之下渲染模版了。但是每当变量变化,整个代码块的内容都会被重新计算innerHTML。但是我们做个实验:

代码语言:javascript
复制
<div id="app"></div>
<script>
    const app = document.querySelector('#app');
    let arr = [];
    for (let k in app) {
      arr.push(k);
    }
    console.log(arr.length, arr);
</script>

单个空div居然有多达293个属性。

实际上,在js只要通过一个对象即可描述上面这个div:

代码语言:javascript
复制
{
  type:'div',
  props:{id:'app'},
  chidren:[]
}

到了MVVM普及的时代,前端开发者都有了共识:

•类似underscore的解决方案,每次渲染的成本太高了!•dom是万恶之源。应极力避免之•编译时,肯定不是全部编译,而应该是部分编译。(按需编译)

这时,mvvm 编译优化就集中在如何更好地按需编译

vue3 编译的要点在于:

•使用js来描述dom(虚拟dom)•数据修改,通过diff算法求出需要修改的最小部分——再进行修改。相当于加了一层“缓存”。

1.3 编译原理

作为前端,学习编译原理可以去阅读一个库的源码:

the-super-tiny-compiler :https://github.com/starkwang/the-super-tiny-compiler-cn

未来允许会写一下对这个库的解读笔记。

Vue3 的内容和之前差不多,还是:

1.模板字符串->抽象语法树(ast,用对象来描述dom)2.cransform(语意转换)3.codeGenerate:生成代码。

最简单的render比如——我需要把js编译下列html

代码语言:javascript
复制
<ul id="ul">
  <li class="item">1</li>
  <li class="item">2</li>
  <li class="item">3</li>
</ul>

抽象之后的js代码(ast)可能是

代码语言:javascript
复制
    const dom = {
      type: 'ul',
      props: {
        id: 'ul'
      },
      children: [{
        type: 'li',
        props: {
          class: 'item',
        },
        children: ['1']
      }, {
        type: 'li',
        props: {
          class: 'item',
        },
        children: ['2']
      }, {
        type: 'li',
        props: {
          class: 'item',
        },
        children: ['3']
      }]
    }

代码是个简单的递归:

代码语言:javascript
复制
    const app = document.querySelector('#app');

    const render = (dom, parentNode) => {
      const { type, props, children } = dom;
      const wrap = document.createElement(dom.type);
      for (let attr in props) {
        wrap.setAttribute(attr, props[attr]);
      }

      if (children && children.length) {
        // if(typeof children == '')
        for (let i = 0; i < children.length; i++) {
          if (typeof children[i] == 'string') {
            wrap.innerHTML = children[i];
          } else {
            render(children[i], wrap);
          }
        }
      }

      parentNode.appendChild(wrap);
    }

    render(dom, app);

1.4 源码导读

打开packages/compiler-dom/src/index.ts

代码语言:javascript
复制
export function compile(
  template: string,
  options: CompilerOptions = {}
): CodegenResult {
  return baseCompile(
    template,
    // ...
  )
}

上述代码提示template是一个字符串。跳转到baseCompile:

代码语言:javascript
复制
// we name it `baseCompile` so that higher order compilers like
// @vue/compiler-dom can export `compile` while re-exporting everything else.
export function baseCompile(
  template: string | RootNode,
  options: CompilerOptions = {}
): CodegenResult {
  const onError = options.onError || defaultOnError
  const isModuleMode = options.mode === 'module'
  /* istanbul ignore if */
  // ...

  // 1.basePaser 抽象语法树(ast)
  const ast = isString(template) ? baseParse(template, options) : template
  const [nodeTransforms, directiveTransforms] = getBaseTransformPreset(
    prefixIdentifiers
  )

  // 2. 语义转换
  transform(
    ast,
    extend({}, options, {
      prefixIdentifiers,
      nodeTransforms: [
        ...nodeTransforms,
        ...(options.nodeTransforms || []) // user transforms
      ],
      directiveTransforms: extend(
        {},
        directiveTransforms,
        options.directiveTransforms || {} // user transforms
      )
    })
  )

  // 3. 生成代码
  return generate(
    ast,
    extend({}, options, {
      prefixIdentifiers
    })
  )
}

反映了编译的三大过程。

2. vDOM

在执行这段代码时,发生了什么?

代码语言:javascript
复制
const App = {
  setup(){
    // ..
    watchEffect(()=>{
      // ..
    })
  }
}

watchEffect内的方法被执行时,意味着数据变化。

这时候响应式就会通知组件更新,具体怎么更新?就会触发vdom的diff算法。

2.1 传统vDOM的性能瓶颈

在传统的vdom(react <=15,vue <=2)中,组件每当收到watcher的依赖,虽然能保证自身按照最小规模的方向去更新数据,但是,仍然避免不了递归遍历整棵树。在这种情况,如果计算耗时于33.3ms(30fps情况下),就会导致肉眼可见的卡顿(丢帧)。

再比如上图,反映的是传统vdom的diff流程,一个dom,性能和模板大小正相关,和动态节点的数量无关。那么可能导致一个情况,一个大组件只有少量动态节点的情况下,依然完整的被遍历。

2.2 极致的按需分配

到了vue3,就不需要遍历整棵树了。

vue早就可以支持jsx了。但在vue3写template,可以获得较jsx更好的性能。

这种追求性能极致的灵感,来源于facebook的开源项目prepack(https://prepack.io/)

Prepack是一个JavaScript源代码优化工具:实际上它是一个JavaScript的部分求值器(Partial Evaluator),可在编译时执行原本在运行时的计算过程,并通过重写JavaScript代码来提高其执行效率。Prepack用简单的赋值序列来等效替换JavaScript代码包中的全局代码,从而消除了中间计算过程以及对象分配的操作。对于重初始化的代码,Prepack可以有效缓存JavaScript解析的结果,使得优化效果最佳。

2.3 vDOM发展简史

说到性能提升,离不开虚拟dom的历史。

Vue1.x时代是没有虚拟dom的概念的。它的核心只有依赖(depends),观察者(watcher)还有真实dom。

如上图,每个动态的节点,都对应一个watcher。数据变了,直接去改dom。但是当节点越来越大,结构愈发复杂,随着watcher都增多,会造成性能雪崩。

而对于React 16.4及以下版本,创造性的提出了虚拟dom的概念。但是,React本身是没有响应式系统的。它的更新,依赖于虚拟dom树的diff算法:

如图,先后两个状态,比较发现不同,则更新。

vue2吸取了react的虚拟dom的核心优点。于是wathcer不再通知到真实dom,只通知到“组件(vdom)”,再通过组件去diff,再触发更新。这个举措让vue实现了质的飞跃。

但是,老版本的react依然存在弱点:如果diff时间超过16.6ms(60fps所需单位时间),就会造成卡顿。于是react16再次创造了fibber架构

所谓fibber树,本质上是一个链表。而链表的特性是可以中断的。当渲染任务超过16.6ms,就把控制权还给主线程。待主线程空闲时,再继续。

而对于vue3来说,提升就在于静态标记。也就是前面所提及的内容。

3. mount & reRender

项目地址:https://github.com/dangjingtao/vue2-vs-vue3.git

我们新建一个项目,直接在项目中引入vue3和vue2.并调用loadash的shuffle方法作为乱序依据。

代码语言:javascript
复制
    // 模板
    const template =
      `<div>
        <h1>item length: {{datas.length}}</h1>
        <p><b>{{action}}</b> tooks {{time}} ms</p><br>
        <button @click="shuffle">shuffle</button>
        <ul v-for="item in datas" :key="item.index">
          <li>{{item.name}}-{{item.index}}</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
        </ul>
      </div>`;

    // 数据生成器
    const getData = (n) => {
      let ret = [];
      for (let i = 0; i < n; i++) {
        ret.push({ name: 'djtao', index: Math.round(1000000 * Math.random()) })
      }
      return ret;
    }

    // 以500条为测试数量
    const datas = getData(500);

生成50,500,5000,50000条数据文件,其中动态节点约占1/4。为便于比较,均采用options API写法。

代码语言:javascript
复制
<!--vue2 -->
<script>
      let s = window.performance.now();

    const vm = new Vue({
      el: '#app',
      template,
      data: {
        action: 'render',
        time: 0,
        datas,
      },
      mounted() {
        this.time = window.performance.now() - s;
      },
      methods: {
        shuffle() {
          this.action = 'shuffle';
          this.datas = _.shuffle(this.datas);
          let s = window.performance.now();
          this.$nextTick(() => {
            this.time = window.performance.now() - s;
          })
        }
      }
    })
</script>

Vue3 写法:

代码语言:javascript
复制
<!--vue3 -->
<script>
    Vue.createApp({
      template,
      data() {
        return {
          action: 'render',
          time: 0,
          datas
        }
      },
      mounted() {
        this.time = window.performance.now() - s;
      },
      methods: {
        shuffle() {
          this.action = 'shuffle';
          this.datas = _.shuffle(this.datas);
          let s = window.performance.now();
          this.$nextTick(() => {
            this.time = window.performance.now() - s;
          })
        }
      }
    }).mount('#app');
</script>

分别测试5次,取平均值。统计如下

数据量(条)

50

500

5000

50000

vue2平均渲染(ms)

18.88

46.26

225.88

1746.78

vue3平均渲染(ms)

23.58

40.32

137.4

900.24

vue2平均乱序(ms)

4.06

17.78

146.42

1935.94

vue3平均乱序(ms)

2.42

13.98

94.92

1328.88

由图可见,在5000及以上条数据量时,vue3比vue3要快50%-100%。

4. SSR

在服务端渲染(ssr)场景下,vue3的性能优势更为明显。

在 https://vue-next-template-explorer.netlify.app/ 沙盒,把选项设置为SSR:

先看纯静态节点的渲染:

代码语言:javascript
复制
<div>
  <div>djtao</div>
  <div>djtao1</div>
  <div>djtao2</div>
</div>

编译之后,发现他们全部被转化为了字符串:

代码语言:javascript
复制
// 编译后
import { mergeProps as _mergeProps } from "vue"
import { ssrResolveCssVars as _ssrResolveCssVars, ssrRenderAttrs as _ssrRenderAttrs } from "@vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs) {
  const _cssVars = ssrResolveCssVars({ color: _ctx.color })
  _push(`<div${_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))}><div>djtao</div><div>djtao1</div><div>djtao2</div></div>`)
}

// Check the console for the AST

接下来手写一下vue的ssr。通过express做服务器。以wrk作为压测工具。

Mac 安装wrk(https://github.com/wg/wrk)

代码语言:javascript
复制
brew install wrk

4.1 ssr@vue2

新建项目ssr 2,安装express/vue/vue-server-renderer/vue-template-compiler

代码语言:javascript
复制
npm init -y
npm i express vue vue-server-renderer vue-template-compiler -S

新建一个server.js

代码语言:javascript
复制
/**
 * server side render(SSR) 
 * seo 首屏渲染的解决方案
 */


// vue3的ssr主要时静态节点字符串,只有一个buffer,不停地推字符串
const App = {
  template:`
    <div>
      <div v-for="n in 1000" :key="n"> 
        <ul>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li style="color:red;" v-for="todo in todos">{{n}}-{{todo}}</li>
        </ul>
      </div>
    </div>
  `,
  data(){
    return {
      todos: ['eating','sleeping'],
    }
  }
}

const express = require('express');
const app = express();

const Vue = require('vue');
const render = require('vue-server-renderer').createRenderer();
const vue2compiler = require('vue-template-compiler');

App.render = new Function(vue2compiler.ssrCompile(App.template).render)

app.get('/',async (req,res)=>{
  let vApp = new Vue(App);
  let html = await render.renderToString(vApp);
  // vue 组件解析为字符串。
  res.send(`
    <h1>vue 2 ssr</h1>
    ${html}
  `);
});

app.listen('9001',err=>{
  if(!err){
    console.log('server started...')
  }
});

看到界面:

Vue2 的服务端渲染就完成了。

执行压测(4进程,100并发,持续15秒):

代码语言:javascript
复制
wrk -t4 -c100 -d15 http://localhost:9001

每秒请求大约在162次。

4.2 ssr@vue3

新建项目ssr 3

代码语言:javascript
复制
npm init -y
npm i express vue@next @vue/server-renderer @vue/compiler-ssr -S

新建server.js

代码语言:javascript
复制
/**
 * server side render(SSR) 
 * seo 首屏渲染的解决方案
 */

// vue3的ssr主要时静态节点字符串,只有一个buffer,不停地推字符串
const App = {
  template:`
    <div>
      <div v-for="n in 1000" :key="n"> 
        <ul>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li>Lorem ipsum dolor sit amet</li>
          <li style="color:red;" v-for="todo in todos">{{n}}-{{todo}}</li>
        </ul>
      </div>
    </div>
  `,
  data(){
    return {
      todos: ['eating','sleeping'],
    }
  }
}

const express = require('express');
const app = express();

const Vue = require('vue');
const render = require('@vue/server-renderer');
const vue3compiler = require('@vue/compiler-ssr');

App.ssrRender = new Function('require',vue3compiler.compile(App.template).code)(require);
app.get('/',async (req,res)=>{
  let vApp = Vue.createApp(App);
  let html = await render.renderToString(vApp);
  // vue 组件解析为字符串。
  res.send(`
    <h1>vue 3 ssr</h1>
    ${html}
  `);
});

app.listen('9002',err=>{
  if(!err){
    console.log('server started...')
  }
});

访问本地9002端口,vue3 ssr就访问成功了。

执行压测(4进程,100并发,持续15秒):

代码语言:javascript
复制
wrk -t4 -c100 -d15 http://localhost:9002

vue3 ssr每秒请求大约在374次。vue3 ssr性能是vue2 2倍以上的差距。

vue3的ssr渲染器的逻辑,是尽可能的把虚拟节点转到字符串。

vue3中复杂组件树,ssr场景下会最大化利用node的异步状态,每个组件是一个buffer, 是一个promise 可以直接await, 服务端任何组件节点,都有可能会有异步数据的依赖。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-07-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一Li小麦 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. compile
    • 1.1 vue3的模板是html吗?
      • 1.2 compile的本质
        • 1.3 编译原理
          • 1.4 源码导读
          • 2. vDOM
            • 2.1 传统vDOM的性能瓶颈
              • 2.2 极致的按需分配
                • 2.3 vDOM发展简史
                • 3. mount & reRender
                • 4. SSR
                  • 4.1 ssr@vue2
                    • 4.2 ssr@vue3
                    相关产品与服务
                    云服务器
                    云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
                    领券
                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档