专栏首页一Li小麦vue 随记(5):性能的飞跃

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

性能的飞跃

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/。

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

<div>
  <div>djtao</div>
  <div>{{age}}</div>
</div>

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

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的时候,会被直接忽略。

我们再从模板中加一段:

<div :id="aaa">aaa</div>
// 编译后
_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语法

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

<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。但是我们做个实验:

<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:

{
  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

<ul id="ul">
  <li class="item">1</li>
  <li class="item">2</li>
  <li class="item">3</li>
</ul>

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

    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']
      }]
    }

代码是个简单的递归:

    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

export function compile(
  template: string,
  options: CompilerOptions = {}
): CodegenResult {
  return baseCompile(
    template,
    // ...
  )
}

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

// 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

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

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方法作为乱序依据。

    // 模板
    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写法。

<!--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 写法:

<!--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:

先看纯静态节点的渲染:

<div>
  <div>djtao</div>
  <div>djtao1</div>
  <div>djtao2</div>
</div>

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

// 编译后
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)

brew install wrk

4.1 ssr@vue2

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

npm init -y
npm i express vue vue-server-renderer vue-template-compiler -S

新建一个server.js

/**
 * 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秒):

wrk -t4 -c100 -d15 http://localhost:9001

每秒请求大约在162次。

4.2 ssr@vue3

新建项目ssr 3

npm init -y
npm i express vue@next @vue/server-renderer @vue/compiler-ssr -S

新建server.js

/**
 * 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秒):

wrk -t4 -c100 -d15 http://localhost:9002

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

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

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

本文分享自微信公众号 - 一Li小麦(gh_c88159ec1309),作者:一li小麦

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-21

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 中介者模式

    在程序员的世界里,一个前端处于研发的名副其实的中心位置(虽然很多人不愿意承认),开发过程会同时接受其他10个对象包括PM,美工,测试,后端乃至前端同事等踢过来的...

    一粒小麦
  • 持久化储存(一)

    https://yeasy.gitbooks.io/docker_practice/install/mac.html

    一粒小麦
  • node服务及前端应用部署

    本文案例取自在笔者腾讯云服务器上的实践。上线部署在大公司里其实是专人操作的,一个产品从构思到发布,许许多多的坑要踩。

    一粒小麦
  • Sublime Text 使用技巧1

    Sublime Text 是一款功能很强大的编辑器,用起来很爽,界面也很华丽。但我看了一系列的学习视频时候,才发现为我对Sublime Text 2的许多功能还...

    王云峰
  • IE6/IE7中li底部4px的Bug

    deepcc
  • 简单轮播图实现

    用户1749219
  • 可视化DDoS全球攻击地图

    DDoS攻击通过分布式的源头针对在线服务发起的网络消耗或资源消耗的攻击,目的是使得目标无法正常提供服务。DDoS攻击主要针对一些重要的目标,从银行系统到新闻站点...

    FB客服
  • Jq实现简单的选项卡功能

    申霖
  • 负margin在页面布局中的应用

    在页面中经常会遇到两列的情况,比如说左侧栏固定宽度,右侧栏自适应宽度,此时可以用flex布局的方式,但是这种方式在ie8上不兼容,但是也可以用table。这里我...

    无邪Z
  • SuperSlide轮播插件滚动高度或宽度不对的问题解决

    SuperSlide 是一款比较实用的轮播插件,网站上常用的“焦点图/幻灯片”“Tab标签切换”“图片滚动”“无缝滚动”等都能实现,兼容包括 IE6 的绝大部分...

    德顺

扫码关注云+社区

领取腾讯云代金券