Bootstrap

Vue开发中常见优化手段总结

Tree Shaking or Trunk

动态引入(Dynamic Imports)
动态引入是指在代码执行过程中,根据需要动态加载模块,而不是在应用启动时一次性加载所有模块。这可以通过JavaScript的import()函数实现,它返回一个Promise对象,允许你异步加载模块。

在Vue中,你可以在组件中使用动态引入来按需加载组件或模块。例如:

// 动态引入一个Vue组件
const MyComponent = () => import('./MyComponent.vue');

export default {
  components: {
    MyComponent
  }
}

在这个例子中,MyComponent组件不会在应用启动时加载,而是在实际需要渲染这个组件的时候才加载。

按需加载(On-Demand Loading)

按需加载是指根据用户的交互或应用的状态来加载资源,这样可以减少应用的初始加载时间,提高用户体验。在Vue应用中,按需加载通常与路由和组件结合使用。

Vue Router支持按需加载,你可以在路由配置中使用动态引入来实现:

const routes = [
  {
    path: '/about',
    component: () => import('./views/AboutView.vue')
  },
  {
    path: '/contact',
    component: () => import('./views/ContactView.vue')
  }
];

import AppView from '../views/AppView.vue'

const gender = 'man'

// Commonjs 相对于 EsModule 的区别?
// tree-shaking 一定要使用 EsModule
// if (gender === 'man') {
//   require('../views/DataSourceContent/DataSourceContent.vue')
// } else {
//   require('../views/DataSourceView.vue')
// }

// chunk
if (gender === 'man') {
  // 实现分片
  // 动态加载
  import('../views/DataSourceContent/DataSourceContent.vue')
} else {
  // 动态加载
  import('../views/DataSourceView.vue')
}

// 从 URL 输入到页面展示,这个过程中发生了什么?

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
     ],
   build: {
     rollupOptions: {
+      treeshake: true,
+      plugins: [
+        // rollupPluginVue(),
+      ],
       output: {
-        manualChunks: {
-          'vue-common-lib': ['vue', 'vue-router', 'pinia'],
-          'react-common-lib': ['react', 'react-dom', '@glideapps/glide-data-grid'],
+        // manualChunks: {
+        //   'vue-common-lib': ['vue', 'vue-router', 'pinia'],
+        //   'react-common-lib': ['react', 'react-dom', '@glideapps/glide-data-grid'],
+        // }
+        manualChunks(id, { getModuleIds, getModuleInfo }) {
+          console.log('🚀 ~ manualChunks ~ getModuleInfo:', id, getModuleIds(), getModuleInfo(id))
+          const deps = []
+          const depSubDepId = new Set()
+          if (id.includes('node_modules')) {
+            if (id.includes('vue')) {
+              return 'vue-common-lib'
+            }
+            if (id.includes('react')) {
+              return 'react-common-lib'
+            }
+          }
         }
       }
     }

动态引入与按需加载的协同工作
动态引入和按需加载可以协同工作,以优化应用的性能和用户体验。通过动态引入,你可以将大型应用拆分成更小的代码块,然后根据用户的行为(如点击不同的路由)按需加载这些代码块。

这种方式的好处包括:

  • 减少初始加载时间:用户在加载应用时不必等待所有代码都加载完成。
  • 节省带宽:用户只下载他们实际需要的代码。
  • 提高缓存效率:浏览器可以更有效地缓存按需加载的代码块。

常见性能分析手段

  • rollup-plugin-visualizer 分析构建
  • vue devtools,google 插件,rerender 进行分析
  • performance
  • lighthouse
  • memory

打包分Trunk

针对 vite 进行配置

import { defineConfig, splitVendorChunkPlugin } from 'vite'

plugins: [
    // vue(),
    // vueJsx(),
    splitVendorChunkPlugin(),
]
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-common-lib': ['vue', 'vue-router', 'pinia'],
          'react-common-lib': ['react', 'react-dom', '@glideapps/glide-data-grid'],
        }
      }
    }
  },

前后对比

在这里插入图片描述

在这里插入图片描述
按需加载组件

// vue router 配置
    {
      path: 'layout',
      name: 'layout',
      component: () => defineAsyncComponent(() => import('../views/PageLayoutView.vue'))
    },

Vue Tools

主要分析 vue 项目有没有频繁的 rerender
假设我们项目中有一个数据是数组类型,在发生数据更新时,我们直接重写整个数组数据,这样会导致整个与数据相关联的组件全部重新渲染

为什么要给元素加 key,就是为了辅助 diff 的过程
vue2:全量 diff,key 来去告知 vue 你的组件更新前后节点发生了什么变化,一旦你的数据全量发生了变化(数组变成了新数组,对象变成了新对象【对象引用地址完全变化了】)
vue3:简单 diff、快速 diff、双端 diff

在Vue中,v-for 指令用于基于源数据多次渲染元素或模板块。在使用 v-for 时,key 是一个非常重要的属性,它的作用主要有以下几点:

提高渲染效率:
Vue在更新动态列表时,使用 key 属性可以帮助Vue精确识别每个节点的身份,从而重用和重新排序现有元素,而不是重新创建所有元素。这可以提高渲染效率,特别是在列表数据频繁变化的场景下。

避免组件/元素的重复渲染:
如果列表中的项目顺序可能会改变,或者项目会动态添加或删除,使用 key 可以确保每个项目有一个独一无二的标识符,Vue可以根据这个标识符决定是否重新渲染组件。

维护组件状态:
在列表中使用组件时,如果子组件有自己的状态,并且这个状态需要在列表更新时保持不变,那么 key 就非常重要。没有 key,Vue可能会错误地复用组件实例,导致组件状态的丢失。

提供唯一的DOM引用:
当你需要通过DOM操作或者事件处理器访问列表中的特定元素时,key 可以帮助Vue为每个元素提供唯一的引用。

避免潜在的bug:
没有 key,Vue在处理列表更新时可能会遇到一些难以追踪的bug,比如元素状态的意外变化或者渲染不正确。

Vue 常见分析思路

通常,针对 Vue3 项目,我们需要考虑较好性能,就需要分析应用性能瓶颈,从而针对渲染与部分逻辑实现,进行改良优化。比如:

  1. 虚拟列表,virtualList https://github.com/tangbc/vue-virtual-scroll-list
  2. 异步组件结合 chunk
  3. canvas 在大数据量内容渲染下的应用
  4. 减少 rerender,分析 props、state
  5. 使用 provide、inject
  6. 解决 vue 组件 props 层层传递的问题,而这个传递过程中,有些组件其实压根儿没有用到对应的 props,他只是起到一个传递的作用,以此性能也受到了牵连
// 针对 Vue2 的同学
// parent component providing 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// child component injecting 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}
provide('editable', false)

// 在需要使用的组件中
inject('editable')
  1. 动态组件合理使用 KeepAlive
  2. 事件清除
  3. 计时器清除,防止内存泄漏
  4. lodash-es 比 lodash 更好

分析包体积
一般的优化思路
10. 分包,chunk
11. asyncComponent,异步导入,动态加载
12. ssr

Keep-alive使用

<LaptopPreviewer
  v-if="previewMode === 'laptop'"
  :key="'laptop'"
  :preview-mode="previewMode"
  @preview-mode-change="handleModeChange"
/>
<MobilePreviewer
  v-if="previewMode === 'mobile'"
  :key="'mobile'"
  :preview-mode="previewMode"
  @preview-mode-change="handleModeChange"
/>

可以修改为

    <KeepAlive>
      <component
        :is="previewMode === 'laptop' ? LaptopPreviewer : MobilePreviewer"
        :preview-mode="previewMode"
        @preview-mode-change="handleModeChange"
      />
    </KeepAlive>

Note:

  • **合理使用:**keep-alive通过缓存已经渲染的组件来提升应用性能,但过多使用可能会导致内存占用增加,因此需要谨慎使用。只在真正需要保持组件状态的场景下使用keep-alive

  • **控制缓存的组件数量:**默认情况下,keep-alive会缓存所有经过它的子组件。如果不需要缓存所有组件,可以通过include和exclude属性来选择性地缓存组件,以避免不必要的内存占用

  • **生命周期的影响:**被keep-alive缓存的组件,会在activated和deactivated生命周期钩子函数中触发相应的逻辑。因此,在使用keep-alive时,要注意这些生命周期函数的使用场景和影响

  • 组件状态更新:由于keep-alive组件对缓存的组件进行了复用,需要小心处理组件状态的更新。一些状态变更操作可能不会在组件重新激活时触发,需要手动处理相应的逻辑

  • **样式与动画:**由于keep-alive会复用组件实例,可能会导致一些样式和动画的问题,特别是涉及到组件之间的切换效果时,需要特别注意相关的样式和动画逻辑

  • **避免内存泄漏:**使用keep-alive时需要注意避免内存泄漏。确保正确配置include和exclude属性,以避免缓存不需要的组件。只缓存需要保留状态的组件,避免缓存不需要的组件导致内存泄漏

  • 动态配置:根据不同的应用场景和用户需求,动态配置include和exclude属性。例如,根据用户的登录状态或访问的页面,动态调整需要缓存的组件

  • **及时清理缓存:**当不再需要缓存某个组件时,及时清理缓存。可以使用v-if或v-show等指令来控制keep-alive的显示和隐藏,以释放不需要的缓存

  • **多层嵌套路由缓存问题:**在多层嵌套路由中,可以通过将所有router-view都通过keep-alive包裹起来,并使用include或exclude属性来判断是否需要缓存

  • 动态组件缓存问题:如果多个路由使用同一个组件,可以通过动态修改组件的名称来解决缓存问题,确保每个路由的组件实例都有唯一的名称,从而正确缓存

事件清除

const resize = () => {
  const isMobile = isMobileTablet()

  if (isMobile) {
    device.value = 'mobile'
  } else {
    device.value = 'laptop'
  }
}

onMounted(() => {
  const isMobile = isMobileTablet()

  if (isMobile) {
    device.value = 'mobile'
  }

  window.addEventListener('resize', resize, false)
})

onUnmounted(() => {
  window.removeEventListener('resize', resize, false)
})

定时器清除

let timer: number | null = null

onMounted(() => {
  // 注意,一定要使用 window.setInterval,否则 ts 类型报错
  timer = window.setInterval(() => {
    const date = new Date()
    time.value = date.toLocaleTimeString()
  }, 1000)
})

onBeforeUnmount(() => {
  timer && window.clearInterval(timer)
})

官方推荐

概述
Vue 在大多数常见场景下性能都是很优秀的,通常不需要手动优化。然而,总会有一些具有挑战性的场景需要进行针对性的微调。在本节中,我们将讨论用 Vue 开发的应用在性能方面该注意些什么。
首先,让我们区分一下 web 应用性能的两个主要方面:

  • 页面加载性能:首次访问时,应用展示出内容与达到可交互状态的速度。这通常会用 Google 所定义的一系列 Web 指标 (Web Vitals) 来进行衡量,如最大内容绘制 (Largest Contentful Paint,缩写为 LCP) 和首次输入延迟 (First Input Delay,缩写为 FID)。
  • 更新性能:应用响应用户输入更新的速度。比如当用户在搜索框中输入时结果列表的更新速度,或者用户在一个单页面应用 (SPA) 中点击链接跳转页面时的切换速度。
    虽然最理想的情况是将两者都最大化,但是不同的前端架构往往会影响到在这些方面是否能达到更理想的性能。此外,你所构建的应用的类型极大地影响了你在性能方面应该优先考虑的问题。因此,优化性能的第一步是为你的应用类型确定合适的架构:
  • 查看使用 Vue 的多种方式这一章看看如何用不同的方式围绕 Vue 组织架构。
  • Jason Miller 在 Application Holotypes 一文中讨论了 Web 应用的类型以及它们各自的理想实现/交付方式。

分析选项

为了提高性能,我们首先需要知道如何衡量它。在这方面,有一些很棒的工具可以提供帮助:
用于生产部署的负载性能分析:

选用正确的架构

如果你的用例对页面加载性能很敏感,请避免将其部署为纯客户端的 SPA,而是让服务器直接发送包含用户想要查看的内容的 HTML 代码。纯客户端渲染存在首屏加载缓慢的问题,这可以通过服务器端渲染 (SSR) 或静态站点生成 (SSG) 来缓解。查看 SSR 指南以了解如何使用 Vue 实现 SSR。如果应用对交互性要求不高,你还可以使用传统的后端服务器来渲染 HTML,并在客户端使用 Vue 对其进行增强。
如果你的主应用必须是 SPA,但还有其他的营销相关页面 (落地页、关于页、博客等),请单独部署这些页面!理想情况下,营销页面应该是包含尽可能少 JS 的静态 HTML,并用 SSG 方式部署。

包体积与 Tree-shaking 优化

一个最有效的提升页面加载速度的方法就是压缩 JavaScript 打包产物的体积。当使用 Vue 时有下面一些办法来减小打包产物体积:

  • 尽可能地采用构建步骤
    • 如果使用的是相对现代的打包工具,许多 Vue 的 API 都是可以被 tree-shake 的。举例来说,如果你根本没有使用到内置的 Transition组件,它将不会被打包进入最终的产物里。Tree-shaking 也可以移除你源代码中其他未使用到的模块。
    • 当使用了构建步骤时,模板会被预编译,因此我们无须在浏览器中载入 Vue 编译器。这在同样最小化加上 gzip 优化下会相对缩小 14kb 并避免运行时的编译开销。
  • 在引入新的依赖项时要小心包体积膨胀!在现实的应用中,包体积膨胀通常因为无意识地引入了过重的依赖导致的。
    • 如果使用了构建步骤,应当尽量选择提供 ES 模块格式的依赖,它们对 tree-shaking 更友好。举例来说,选择 lodash-es 比 lodash 更好。
    • 查看依赖的体积,并评估与其所提供的功能之间的性价比。如果依赖对 tree-shaking 友好,实际增加的体积大小将取决于你从它之中导入的 API。像 bundlejs.com 这样的工具可以用来做快速的检查,但是根据实际的构建设置来评估总是最准确的。
  • 如果你只在渐进式增强的场景下使用 Vue,并想要避免使用构建步骤,请考虑使用 petite-vue (只有 6kb) 来代替。

代码分割

代码分割是指构建工具将构建后的 JavaScript 包拆分为多个较小的,可以按需或并行加载的文件。通过适当的代码分割,页面加载时需要的功能可以立即下载,而额外的块只在需要时才加载,从而提高性能。
像 Rollup (Vite 就是基于它之上开发的) 或者 webpack 这样的打包工具可以通过分析 ESM 动态导入的语法来自动进行代码分割:

// lazy.js 及其依赖会被拆分到一个单独的文件中
// 并只在 `loadLazy()` 调用时才加载
function loadLazy() {
  return import('./lazy.js')
}

懒加载对于页面初次加载时的优化帮助极大,它帮助应用暂时略过了那些不是立即需要的功能。在 Vue 应用中,这可以与 Vue 的异步组件搭配使用,为组件树创建分离的代码块:

import { defineAsyncComponent } from 'vue'

// 会为 Foo.vue 及其依赖创建单独的一个块
// 它只会按需加载
//(即该异步组件在页面中被渲染时)
const Foo = defineAsyncComponent(() => import('./Foo.vue'))

懒加载对于页面初次加载时的优化帮助极大,它帮助应用暂时略过了那些不是立即需要的功能。在 Vue 应用中,这可以与 Vue 的异步组件搭配使用,为组件树创建分离的代码块:

import { defineAsyncComponent } from 'vue'

// 会为 Foo.vue 及其依赖创建单独的一个块
// 它只会按需加载
//(即该异步组件在页面中被渲染时)
const Foo = defineAsyncComponent(() => import('./Foo.vue'))

对于使用了 Vue Router 的应用,强烈建议使用异步组件作为路由组件。Vue Router 已经显性地支持了独立于 defineAsyncComponent 的懒加载。查看懒加载路由了解更多细节。

更新优化

Props 稳定性
在 Vue 之中,一个子组件只会在其至少一个 props 改变时才会更新。思考以下示例:

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

在 ListItem 组件中,它使用了 id 和 activeId 两个 props 来确定它是否是当前活跃的那一项。虽然这是可行的,但问题是每当 activeId 更新时,列表中的每一个 ListIte 都会跟着更新!
理想情况下,只有活跃状态发生改变的项才应该更新。我们可以将活跃状态比对的逻辑移入父组件来实现这一点,然后让 ListItem 改为接收一个 active prop:

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

现在,对于大多数的组件来说,activeId 改变时,它们的 active prop 都会保持不变,因此它们无需再更新。总结一下,这个技巧的核心思想就是让传给子组件的 props 尽量保持稳定。

v-once

v-once 是一个内置的指令,可以用来渲染依赖运行时数据但无需再更新的内容。它的整个子树都会在未来的更新中被跳过。查看它的 API 参考手册可以了解更多细节。

v-memo

v-memo 是一个内置指令,可以用来有条件地跳过某些大型子树或者 v-for 列表的更新。查看它的 API 参考手册可以了解更多细节。

const seen = new WeakSet()

export const transformMemo: NodeTransform = (node, context) => {
  if (node.type === NodeTypes.ELEMENT) {
    const dir = findDir(node, 'memo')
    if (!dir || seen.has(node)) {
      return
    }
    seen.add(node)
    return () => {
      const codegenNode =
        node.codegenNode ||
        (context.currentNode as PlainElementNode).codegenNode
      if (codegenNode && codegenNode.type === NodeTypes.VNODE_CALL) {
        // non-component sub tree should be turned into a block
        if (node.tagType !== ElementTypes.COMPONENT) {
          convertToBlock(codegenNode, context)
        }
        node.codegenNode = createCallExpression(context.helper(WITH_MEMO), [
          dir.exp!,
          createFunctionExpression(undefined, codegenNode),
          `_cache`,
          String(context.cached++)
        ]) as MemoExpression
      }
    }
  }
}

WeakSet
WeakSet、WeakMap、Set、Map 他们有什么区别?
Weakxxx 都属于弱引用
https://zh.javascript.info/weakmap-weakset

通用优化

以下技巧能同时改善页面加载和更新性能。

大型虚拟列表
所有的前端应用中最常见的性能问题就是渲染大型列表。无论一个框架性能有多好,渲染成千上万个列表项都会变得很慢,因为浏览器需要处理大量的 DOM 节点。
但是,我们并不需要立刻渲染出全部的列表。在大多数场景中,用户的屏幕尺寸只会展示这个巨大列表中的一小部分。我们可以通过列表虚拟化来提升性能,这项技术使我们只需要渲染用户视口中能看到的部分。
要实现列表虚拟化并不简单,幸运的是,你可以直接使用现有的社区库:

减少大型不可变数据的响应性开销
Vue 的响应性系统默认是深度的。虽然这让状态管理变得更直观,但在数据量巨大时,深度响应性也会导致不小的性能负担,因为每个属性访问都将触发代理的依赖追踪。好在这种性能负担通常只有在处理超大型数组或层级很深的对象时,例如一次渲染需要访问 100,000+ 个属性时,才会变得比较明显。因此,它只会影响少数特定的场景。
Vue 确实也为此提供了一种解决方案,通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的,对所有深层的对象不会做任何处理。这使得对深层级属性的访问变得更快,但代价是,我们现在必须将所有深层级对象视为不可变的,并且只能通过替换整个根状态来触发更新:

const shallowArray = shallowRef([
  /* 巨大的列表,里面包含深层的对象 */
])

// 这不会触发更新...
shallowArray.value.push(newObject)
// 这才会触发更新
shallowArray.value = [...shallowArray.value, newObject]

// 这不会触发更新...
shallowArray.value[0].foo = 1
// 这才会触发更新
shallowArray.value = [
  {
    ...shallowArray.value[0],
    foo: 1
  },
  ...shallowArray.value.slice(1)
]

避免不必要的组件抽象

有些时候我们会去创建无渲染组件或高阶组件 (用来渲染具有额外 props 的其他组件) 来实现更好的抽象或代码组织。虽然这并没有什么问题,但请记住,组件实例比普通 DOM 节点要昂贵得多,而且为了逻辑抽象创建太多组件实例将会导致性能损失。
需要提醒的是,只减少几个组件实例对于性能不会有明显的改善,所以如果一个用于抽象的组件在应用中只会渲染几次,就不用操心去优化它了。考虑这种优化的最佳场景还是在大型列表中。想象一下一个有 100 项的列表,每项的组件都包含许多子组件。在这里去掉一个不必要的组件抽象,可能会减少数百个组件实例的无谓性能消耗。

;