5

从面试题入手,畅谈 Vue 3 性能优化 - muwooo

 1 year ago
source link: https://www.cnblogs.com/muwoo/p/17043930.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

今年又是一个非常寒冷的冬天,很多公司都开始人员精简。市场从来不缺前端,但对高级前端的需求还是特别强烈的。一些大厂的面试官为了区分候选人对前端领域能力的深度,经常会在面试过程中考察一些前端框架的源码性知识点。Vuejs 作为世界顶尖的框架之一,几乎在所有的面试场景中或多或少都会被提及。

笔者之前在蚂蚁集团就职,对于 Vue 3 的考点还是会经常问的。接下来,我将根据多年的面试以及被面试经验,为小伙伴们梳理最近大厂爱问的 Vue 3 问题。然后我们再根据问题举一反三,深入学习 Vue 3 源码知识!

场景一:Vue 3.x 相对于 Vue 2.x 做了那些额外的性能优化?

要理解 Vue 3 的性能优化的核心,就需要了解 Vuejs 的核心设计理念。我们知道 Vuejs 官网上有一句话总结的特别到位:渐进式 JavaScript 框架,易学易用,性能出色,适用于场景丰富的 Web 框架。 其实我们的答案就蕴藏在这句话里。

首先,我们知道当我们浏览 Web 网页时,有两类场景会制约 Web 网页的性能

  1. 网络传输的瓶颈
  2. CPU的瓶颈

所以要回答这个问题,就可以直接从这两方面入手。

网络传输的瓶颈优化

对于前端框架而言,制约网络传输的因素最大的就是代码体积,代码体积越大,传输效率越慢。尤其对于 SPA 单页应用的 CSR(客户端渲染) 而言。一个大体积的框架资源,就意味着用户需要等待白屏的时间越长。而 Vue 3 在减少源码体积方面做的最多的就是通过精细化的 Tree-Shacking 机制来构建 渐进式 代码。

1. /*#__PURE__*/ 标记

我们知道 Tree-Shaking 可以删除一些 DC(dead code) 代码。但是对于一些有副作用的函数代码,却是无法进行很好的识别和删除,举个例子:

foo()

function foo(obj) {
  obj?.a
}

上述代码中,foo 函数本身是没有任何意义的,仅仅是对对象 obj 进行了属性 a 的读取操作,但是 Tree-Shaking 是无法删除该函数的,因为上述的属性读取操作可能会产生副作用,因为 obj 可能是一个响应式对象,我们可能对 obj 定了一个 gettergetter 中触发了很多不可预期的操作。

如果我们确认 foo 函数是一个不会有副作用的纯净的函数,那么这个时候 /*#__PURE__*/ 就派上用场了,其作用就是告诉打包器,对于 foo 函数的调用不会产生副作用,你可以放心地对其进行 Tree-Shaking

另外,值得一提的是,在 Vue 3 源码中,包含了大量的 /*#__PURE__*/ 标识符,可见 Vue 3 对源码体积的控制是多么的用心!

2. 特性开关

Vue 3 源码中的 rollup.config.mjs 中有这样一段代码:

{
  __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
}

其中 __FEATURE_OPTIONS_API__ 是一个构建时的环境变量,我们知道 Vue 3 在某些 API 方面是兼容 Vue 3 写法的,比如 Options API。但是如果我们在项目中仅仅使用 Compositon API 而不想使用 Options API 那么我们就可以在项目构建时关闭这个选项,从而减少代码体积。我们看看这个变量在 Vue 3 源码中是如何使用的:

// 兼容 2.x 选项式 API
if (__FEATURE_OPTIONS_API__) {
  currentInstance = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}

用户可以通过设置 __VUE_OPTIONS_API__ 预定义常量的值来控制是否要包含这段代码。通常用户可以使用 webpack.DefinePlugin 插件来实现:

// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
  __VUE_OPTIONS_API__: JSON.stringify(true) // 开启特性
})

除此之外,类似的开发环境会通过 __DEV__ 来输出告警规则,而在生产环境剔除这些告警降低构建后的包体积都是类似的手段:

if (__DEV__) {
  console.warn(`value cannot be made reactive: ${String(target)}`)
}

CPU 瓶颈优化

当项目变得庞大、组件数量繁多时,就容易遇到CPU的瓶颈。主流浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次。

我们知道,JS可以操作DOM,GUI渲染线程JS线程是互斥的。所以JS脚本执行浏览器布局、绘制不能同时执行。

在每16.6ms时间内,需要完成如下工作:

JS脚本执行 -----  样式布局 ----- 样式绘制

当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局样式绘制了,也就出现了丢帧的情况,会发生卡顿。

为了解决庞大元素组件渲染、更新卡顿的问题,Vue 的策略是一方面采用了组件级的细粒度更新,控制更新的影响面:Vue 3 中,每个组件都会生成一个渲染函数,这些渲染函数执行时会进行数据访问,此时这些渲染函数被收集进入副作用函数中,建立数据 -> 副作用的映射关系。当数据变更时,再触发副作用函数的重新执行,即重新渲染。

image.png

另一方面则在编译器中做了大量的静态优化,得益于这些优化,才让我们可以 易学易用的写出性能出色的 Vue 项目。 下面简单介绍几种编译时优化策略:

1. 靶向更新

假设有以下模板:

<template>
  <p>hello world</p>
  <p>{{ msg }}</p>
</template>>

其中一个 p 标签的节点是一个静态的节点,第二个 p 标签的节点是一个动态的节点,如果当 msg 的值发生了变化,那么理论上肉眼可见最优的更新方案应该是只做第二个动态节点的 diff 而无需进行第一个 p 标签节点的 diff

上述模版转成 vnode 后的结果大致为:

const vnode = {
  type: Symbol(Fragment),
  children: [
    { type: 'p', children: 'hello world' },
    { type: 'p', children: ctx.msg, patchFlag: 1 /* 动态的 text */ },
  ],
  dynamicChildren: [
    { type: 'p', children: ctx.msg, patchFlag: 1 /* 动态的 text */ },
  ]
}

此时组件内存在了一个静态的节点 <p>hello world</p>,在传统的 diff 算法里,还是需要对该静态节点进行不必要的 diff

Vue3 则是先通过 patchFlag 来标记动态节点 <p>{{ msg }}</p> 然后配合 dynamicChildren 将动态节点进行收集,从而完成在 diff 阶段只做靶向更新的目的。

2. 静态提升

接下来,我们再来说一下,为什么要做静态提升呢? 如下模板所示:

<div>
  <p>text</p>
</div>

在没有被提升的情况下其渲染函数相当于:

import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("p", null, "text")
  ]))
}

很明显,p 标签是静态的,它不会改变。但是如上渲染函数的问题也很明显,如果组件内存在动态的内容,当渲染函数重新执行时,即使 p 标签是静态的,那么它对应的 VNode 也会重新创建。

所谓的 “静态提升”,就是将一些静态的节点或属性提升到渲染函数之外。如下面的代码所示:

import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, "text", -1 /* HOISTED */)
const _hoisted_2 = [
  _hoisted_1
]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}

这就实现了减少 VNode 创建的性能消耗。

而这里的静态提升步骤生成的 hoists,会在 codegenNode 会在生成代码阶段帮助我们生成静态提升的相关代码。

预字符串化

Vue 3 在编译时会进行静态提升节点的 预字符串化。什么是预字符串化呢?一起来看个示例:

<template>
  <p></p>
  ... 共 20+ 节点
  <p></p>
</template>

对于这样有大量静态提升的模版场景,如果不考虑 预字符串化 那么生成的渲染函数将会包含大量的 createElementVNode 函数:假设如上模板中有大量连续的静态的 p 标签,此时渲染函数生成的结果如下:

const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, null, -1 /* HOISTED */)
// ...
const _hoisted_20 = /*#__PURE__*/_createElementVNode("p", null, null, -1 /* HOISTED */)
const _hoisted_21 = [
  _hoisted_1,
  // ...
  _hoisted_20,
]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _hoisted_21))
}

createElementVNode 大量连续性创建 vnode 也是挺影响性能的,所以可以通过 预字符串化 来一次性创建这些静态节点,采用 与字符串化 后,生成的渲染函数如下:

const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<p></p>...<p></p>", 20)
const _hoisted_21 = [
  _hoisted_1
]

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _hoisted_21))
}

这样一方面降低了 createElementVNode 连续创建带来的性能损耗,也侧面减少了代码体积。

本小节为大家解读了部分 Vue 3 性能优化的设计,更多的内容可以参考作者写的小册:《Vue 3 技术揭秘》

另外,附上小册 50 个 6 折码:jQqasaoW

数量有限。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK