跳转到内容

性能

概述

Vue被设计成在大多数常见用例中性能良好,无需进行大量手动优化。然而,总有一些具有挑战性的场景需要额外的精细调整。在本节中,我们将讨论在Vue应用中关于性能你应该关注什么。

首先,让我们讨论两个主要方面:网页性能

  • 页面加载性能:应用程序在初次访问时显示内容并变得可交互的速度。这通常使用如最大内容渲染时间(LCP)首次输入延迟(FID)这样的网络关键指标来衡量。

  • 更新性能:应用程序响应用户输入时的速度。例如,当用户在搜索框中键入时列表更新的速度,或者当用户在单页面应用程序(SPA)中点击导航链接时页面切换的速度。

虽然最大化这两个方面是理想的,但不同的前端架构往往会影响在这些方面获得期望性能的难易程度。此外,你正在构建的应用程序的类型也极大地影响了你在性能方面应该优先考虑的内容。因此,确保最佳性能的第一步是选择适合你正在构建的应用程序类型的最合适的架构。

  • 请参阅Vue的使用方式,了解您如何以不同的方式利用Vue。

  • Jason Miller在应用全息类型中讨论了不同类型的Web应用程序及其各自理想的实现/交付方式。

配置文件分析

为了提高性能,我们首先需要了解如何衡量它。有很多优秀的工具可以帮助在这方面。

用于分析生产部署的负载性能

用于分析本地开发时的性能

页面加载优化

优化页面加载性能有许多与框架无关的方面 - 查看此web.dev 指南以获取全面的概述。在此,我们将主要关注Vue特有的技术。

选择合适的架构

如果您的用例对页面加载性能敏感,请避免将其作为纯客户端SPA发布。您希望服务器直接发送包含用户想要查看的内容的HTML。纯客户端渲染由于内容加载时间慢而受到限制。这可以通过服务器端渲染(SSR)静态站点生成(SSG)来缓解。查看SSR 指南以了解如何使用Vue进行SSR。如果您的应用程序没有丰富的交互性需求,您也可以使用传统的后端服务器来渲染HTML,并在客户端使用Vue进行增强。

如果您的主应用程序必须是一个SPA,但包含营销页面(着陆页、关于、博客),请分别发布它们!理想情况下,您的营销页面应作为具有最少JS的静态HTML通过SSG进行部署。

包大小和摇树优化

提高页面加载性能的最有效方法之一是发布更小的JavaScript包。以下是一些在Vue中使用时减少包大小的几种方法

  • 如果可能的话,使用构建步骤。

    • 许多Vue的API在通过现代构建工具打包时是"摇树可用的"。例如,如果您不使用内置的<Transition>组件,它将不会包含在最终的生产包中。摇树也可以从源代码中移除其他未使用的模块。

    • 在构建步骤中,模板被预编译,因此我们不需要将Vue编译器发送到浏览器。这节省了14kb min+gzip压缩的JavaScript,并避免了运行时编译的成本。

  • 在引入新依赖时,请小心其大小!在实际应用程序中,膨胀的包通常是由于无意中引入了重量级的依赖项。

    • 如果使用构建步骤,请首选提供ES模块格式且对摇树友好的依赖项。例如,首选lodash-es而不是lodash

    • 检查依赖项的大小,并评估其提供的功能是否值得。注意如果依赖项是摇树友好的,实际的尺寸增加将取决于您从中实际导入的API。例如,可以使用bundlejs.com等工具进行快速检查,但使用您的实际构建配置进行测量始终是最准确的。

  • 如果您主要使用Vue进行渐进式增强并希望避免构建步骤,请考虑使用petite-vue(仅6kb)。

代码拆分

代码拆分是指构建工具将应用程序包拆分为多个更小的块,这些块可以按需或并行加载。通过适当的代码拆分,可以在页面加载时立即下载所需的功能,仅在需要时才懒加载额外的块,从而提高性能。

如Rollup(Vite基于其构建)或webpack之类的打包器可以自动通过检测ESM动态导入语法创建拆分块。

js
// lazy.js and its dependencies will be split into a separate chunk
// and only loaded when `loadLazy()` is called.
function loadLazy() {
  return import('./lazy.js')
}

懒加载最适合用于在初始页面加载后不需要立即使用的功能。在Vue应用程序中,这可以与Vue的异步组件功能结合使用,为组件树创建拆分块。

js
import { defineAsyncComponent } from 'vue'

// a separate chunk is created for Foo.vue and its dependencies.
// it is only fetched on demand when the async component is
// rendered on the page.
const Foo = defineAsyncComponent(() => import('./Foo.vue'))

对于使用Vue Router的应用程序,强烈建议使用懒加载来处理路由组件。Vue Router对懒加载有显式支持,与defineAsyncComponent分开。有关更多详细信息,请参阅懒加载路由

更新优化

Props稳定性

在Vue中,子组件仅在至少一个接收到的属性发生变化时才会更新。考虑以下示例

template
<ListItem
  v-for="item in list"
  :id="item.id"
  :active-id="activeId" />

<ListItem>组件内部,它使用其idactiveId属性来确定它是否是当前活动项。虽然这样可以工作,但问题是每次activeId发生变化时,列表中的所有<ListItem>都必须更新!

理想情况下,只有状态发生变化的活动项需要更新。我们可以通过将活动状态计算移动到父组件,并使<ListItem>直接接受一个active属性来实现这一点。

template
<ListItem
  v-for="item in list"
  :id="item.id"
  :active="item.id === activeId" />

现在,对于大多数组件,当activeId发生变化时,active属性将保持不变,因此它们不再需要更新。总的来说,保持传递给子组件的属性尽可能稳定是关键。

v-once

v-once是一个内置指令,可用于渲染依赖于运行时数据但永远不会需要更新的内容。使用它的整个子树将跳过所有未来的更新。有关更多详细信息,请参阅其API参考

v-memo

v-memo是一个内置指令,可用于有条件地跳过大子树或v-for列表的更新。有关更多详细信息,请参阅其API参考

计算稳定性

在Vue 3.4及以上版本中,计算属性只有在计算值从上一个值更改时才会触发副作用。例如,以下isEven计算属性只有在返回值从true变为false或相反时才会触发副作用。

js
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)

watchEffect(() => console.log(isEven.value)) // true

// will not trigger new logs because the computed value stays `true`
count.value = 2
count.value = 4

这减少了不必要的副作用触发,但不幸的是,如果计算创建每个计算的新对象,则不起作用。

js
const computedObj = computed(() => {
  return {
    isEven: count.value % 2 === 0
  }
})

由于每次都会创建一个新的对象,新值在技术上总是与旧值不同。即使isEven属性保持不变,Vue也无法知道,除非它执行旧值和新值之间的深度比较。这种比较可能代价高昂,而且可能不值得。

相反,我们可以通过手动比较新值与旧值来优化这个过程,如果我们知道没有变化,就条件性地返回旧值

js
const computedObj = computed((oldValue) => {
  const newValue = {
    isEven: count.value % 2 === 0
  }
  if (oldValue && oldValue.isEven === newValue.isEven) {
    return oldValue
  }
  return newValue
})

在游乐场试一试

请注意,您应该总是在比较和返回旧值之前执行完整的计算,以便每次运行都能收集相同的依赖项。

通用优化

以下提示会影响页面加载和更新性能。

虚拟化大型列表

所有前端应用中最常见的性能问题是渲染大型列表。无论框架有多高效,渲染包含数千个项目的列表都会很慢,因为浏览器需要处理的DOM节点数量太多。

然而,我们并不一定需要一开始就渲染所有这些节点。在大多数情况下,用户的屏幕大小只能显示我们大型列表的一小部分。我们可以通过使用**列表虚拟化**技术,仅在大列表中渲染当前位于或接近视口的项,从而显著提高性能。

实现列表虚拟化并不容易,幸运的是,有一些现有的社区库可以直接使用

减少大型不可变结构反应性开销

Vue的反应性系统默认是深层次的。虽然这使得状态管理直观,但当数据量很大时,它确实会创建一定程度的开销,因为每次属性访问都会触发代理陷阱以执行依赖跟踪。这通常在处理包含100,000+属性的大型嵌套对象数组时变得明显,因此它只会影响非常特定的用例。

Vue确实提供了一个逃生舱,可以通过使用shallowRef()shallowReactive()退出深度反应性。浅API创建的只在最顶层是反应性的状态,并暴露所有未触及的嵌套对象。这保持了嵌套属性访问的速度,但代价是我们现在必须将所有嵌套对象视为不可变的,并且更新只能通过替换根状态来触发

js
const shallowArray = shallowRef([
  /* big list of deep objects */
])

// this won't trigger updates...
shallowArray.value.push(newObject)
// this does:
shallowArray.value = [...shallowArray.value, newObject]

// this won't trigger updates...
shallowArray.value[0].foo = 1
// this does:
shallowArray.value = [
  {
    ...shallowArray.value[0],
    foo: 1
  },
  ...shallowArray.value.slice(1)
]

避免不必要的组件抽象

有时我们可能会创建无渲染组件或高阶组件(即通过额外属性渲染其他组件的组件),以实现更好的抽象或代码组织。虽然这没什么问题,但请记住,组件实例比普通DOM节点要昂贵得多,并且由于抽象模式创建太多实例将产生性能成本。

请注意,仅减少少量实例不会产生明显效果,因此如果组件在应用程序中只渲染几次,请不要担心。考虑这种优化的最佳场景仍然是大型列表。想象一下有100个项目的列表,其中每个项目组件包含许多子组件。在这里移除一个不必要的组件抽象可能会减少数百个组件实例。

性能已加载