MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue CLI 性能优化与内存管理技巧分享

2021-11-101.9k 阅读

Vue CLI 性能优化技巧

代码拆分与懒加载

在 Vue 项目中,随着功能的增加,打包后的文件体积也会不断增大。这会导致页面加载时间变长,影响用户体验。代码拆分与懒加载是解决这一问题的有效方法。

Vue Router 提供了一种简单的方式来实现路由组件的懒加载。在定义路由时,可以使用 import() 语法来动态导入组件。例如:

const router = new VueRouter({
  routes: [
    {
      path: '/home',
      name: 'Home',
      component: () => import('./components/Home.vue')
    },
    {
      path: '/about',
      name: 'About',
      component: () => import('./components/About.vue')
    }
  ]
})

这样,只有当用户访问对应的路由时,才会加载相应的组件,而不是在页面加载时一次性加载所有组件。

对于非路由组件,也可以使用 import() 进行懒加载。比如在一个复杂的页面中,有一些组件只有在特定条件下才会使用到,可以将这些组件进行懒加载。例如:

<template>
  <div>
    <button @click="loadComponent">加载组件</button>
    <component :is="loadedComponent" v-if="loadedComponent"></component>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loadedComponent: null
    }
  },
  methods: {
    async loadComponent() {
      const { default: LazyComponent } = await import('./components/LazyComponent.vue')
      this.loadedComponent = LazyComponent
    }
  }
}
</script>

在上述代码中,只有当用户点击按钮时,才会加载 LazyComponent.vue 组件。

Tree Shaking

Tree Shaking 是一种通过消除未使用代码来优化打包体积的技术。在 Vue CLI 项目中,它依赖于 ES6 模块的静态结构分析。

要启用 Tree Shaking,首先确保项目使用 ES6 模块语法导入和导出。例如,在一个工具函数文件 utils.js 中:

export const add = (a, b) => a + b
export const subtract = (a, b) => a - b

在另一个文件中,如果只使用 add 函数:

import { add } from './utils.js'
const result = add(2, 3)

Webpack 在打包时,会分析模块依赖关系,发现 subtract 函数未被使用,从而将其从打包结果中移除。

为了让 Tree Shaking 更有效,建议在库的开发中使用 ES6 模块语法,并且避免使用动态导入(除非是为了懒加载),因为动态导入会破坏静态分析,使得 Tree Shaking 无法正常工作。

优化图片资源

图片往往是网页中占用体积较大的资源,优化图片对于提升性能至关重要。

  1. 图片格式选择:不同的图片格式适用于不同的场景。例如,JPEG 适用于色彩丰富的照片,PNG 适用于有透明度要求的图片或者简单的图标。对于一些简单的图形,还可以考虑使用 SVG,它是矢量图形,无论如何缩放都不会失真,并且文件体积通常较小。

  2. 图片压缩:可以使用工具对图片进行压缩。在 Vue CLI 项目中,可以使用 image-webpack-loader 插件。首先安装该插件:

npm install image-webpack-loader --save-dev

然后在 webpack.config.js 中配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              name: 'images/[name].[ext]'
            }
          },
          {
            loader: 'image-webpack-loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 65
              },
              // optipng.enabled: false will disable optipng
              optipng: {
                enabled: false
              },
              pngquant: {
                quality: [0.65, 0.90],
                speed: 4
              },
              gifsicle: {
                interlaced: false
              },
              // the webp option will enable WEBP
              webp: {
                quality: 75
              }
            }
          }
        ]
      }
    ]
  }
}

上述配置会对 PNG、JPEG 和 GIF 图片进行压缩,在保证图片质量的前提下减小文件体积。

优化 CSS

  1. 避免全局样式:在 Vue 组件中,尽量使用 scoped CSS。通过在 <style> 标签上添加 scoped 属性,CSS 样式只会应用到当前组件。例如:
<template>
  <div class="my-component">
    <p>这是组件内的文本</p>
  </div>
</template>

<style scoped>
.my-component {
  color: blue;
}
</style>

这样可以避免样式污染,并且在打包时,样式文件也会更加精简。

  1. CSS 压缩:Vue CLI 默认会对 CSS 进行压缩。在 vue.config.js 中,可以进一步配置 CSS 压缩选项。例如:
module.exports = {
  css: {
    extract: true,
    minimize: true,
    sourceMap: false,
    loaderOptions: {
      css: {},
      postcss: {
        plugins: [
          require('cssnano')({
            preset: 'default'
          })
        ]
      }
    }
  }
}

cssnano 是一个 CSS 优化工具,它可以压缩 CSS 代码,移除未使用的 CSS 规则等。

使用 SSR(服务器端渲染)

SSR 可以在服务器端生成 HTML 页面,然后将其发送到客户端。这样,在页面加载时,用户可以更快地看到内容,特别是对于首屏渲染有很大的提升。

  1. 配置 SSR 项目:首先需要使用 vue - cli 脚手架创建一个 SSR 项目:
vue create -p vue - js/ssr my - ssr - project
cd my - ssr - project
  1. 理解 SSR 原理:在 SSR 项目中,Vue 组件会在服务器端渲染成 HTML 字符串,然后发送到客户端。客户端接收到 HTML 后,会进行“激活”操作,将静态的 HTML 转化为可交互的 Vue 应用。例如,一个简单的 SSR 组件:
<template>
  <div>
    <h1>{{ message }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: '这是 SSR 渲染的内容'
    }
  }
}
</script>

在服务器端,会根据组件的状态生成相应的 HTML:

<div>
  <h1>这是 SSR 渲染的内容</h1>
</div>

然后发送到客户端。客户端通过 Vue 的 hydration 机制,将这个 HTML 激活为一个 Vue 应用,使得页面可以进行交互。

  1. 优化 SSR 性能:在 SSR 项目中,可以对一些数据进行缓存。例如,对于一些不经常变化的 API 数据,可以在服务器端进行缓存,避免每次请求都重新获取数据。另外,合理配置服务器资源,如使用 CDN 加速静态资源的加载,也能提升 SSR 应用的性能。

Vue CLI 内存管理技巧

理解 Vue 的响应式原理与内存占用

Vue 通过 Object.defineProperty() 方法来实现数据的响应式。当一个对象被定义为响应式时,Vue 会为对象的每个属性添加 getter 和 setter 方法。这意味着,随着项目中数据量的增加,内存占用也会相应增加。

例如,定义一个简单的 Vue 实例:

const vm = new Vue({
  data() {
    return {
      user: {
        name: '张三',
        age: 20,
        address: '北京市'
      }
    }
  }
})

在这个例子中,user 对象的每个属性都被转化为响应式数据,Vue 会为这些属性添加额外的 getter 和 setter 方法,从而占用一定的内存。

避免内存泄漏

  1. 事件绑定与解绑:在 Vue 组件中,经常会绑定一些 DOM 事件。如果在组件销毁时没有解绑这些事件,就会导致内存泄漏。例如:
<template>
  <div>
    <button @click="handleClick">点击</button>
  </div>
</template>

<script>
export default {
  methods: {
    handleClick() {
      console.log('按钮被点击')
    }
  },
  mounted() {
    window.addEventListener('resize', this.handleResize)
  },
  methods: {
    handleResize() {
      console.log('窗口大小改变')
    },
    beforeDestroy() {
      window.removeEventListener('resize', this.handleResize)
    }
  }
}
</script>

在上述代码中,在 mounted 钩子函数中绑定了 windowresize 事件,在 beforeDestroy 钩子函数中解绑了该事件,避免了内存泄漏。

  1. 清除定时器:如果在组件中使用了定时器,同样需要在组件销毁时清除定时器。例如:
<template>
  <div>
    <p>当前时间: {{ currentTime }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      currentTime: new Date()
    }
  },
  mounted() {
    this.timer = setInterval(() => {
      this.currentTime = new Date()
    }, 1000)
  },
  beforeDestroy() {
    clearInterval(this.timer)
  }
}
</script>

这里在 mounted 中启动了一个定时器,在 beforeDestroy 中清除了定时器,防止内存泄漏。

组件销毁与内存释放

  1. 正确使用 v - ifv - showv - if 会根据条件动态地创建或销毁组件,而 v - show 只是通过 CSS 的 display 属性来显示或隐藏组件。如果一个组件在某些条件下不再需要使用,并且希望释放其占用的内存,应该使用 v - if。例如:
<template>
  <div>
    <button @click="toggleComponent">切换组件</button>
    <MyComponent v - if="isComponentVisible"></MyComponent>
  </div>
</template>

<script>
import MyComponent from './MyComponent.vue'
export default {
  components: {
    MyComponent
  },
  data() {
    return {
      isComponentVisible: true
    }
  },
  methods: {
    toggleComponent() {
      this.isComponentVisible =!this.isComponentVisible
    }
  }
}
</script>

isComponentVisiblefalse 时,MyComponent 会被销毁,其占用的内存会被释放。

  1. 使用 keep - alive 时的内存管理keep - alive 组件可以缓存组件实例,避免重复创建和销毁。但这也意味着被缓存的组件实例仍然占用内存。在使用 keep - alive 时,需要谨慎考虑哪些组件适合缓存。例如,对于一些占用内存较大且不经常切换的组件,可以使用 keep - alive 来提高性能,但对于一些临时使用且占用内存较大的组件,可能不适合使用 keep - alive
<template>
  <div>
    <keep - alive>
      <router - view></router - view>
    </keep - alive>
  </div>
</template>

在上述代码中,router - view 中的组件会被缓存,当切换路由时,如果组件在缓存中,则直接从缓存中获取,而不是重新创建。但如果缓存的组件过多,可能会导致内存占用过高,此时可以通过设置 max 属性来限制缓存的组件数量:

<template>
  <div>
    <keep - alive :max="10">
      <router - view></router - view>
    </keep - alive>
  </div>
</template>

这样,当缓存的组件数量超过 10 个时,会按照 LRU(最近最少使用)原则移除最久未使用的组件,释放内存。

优化数据结构以减少内存占用

  1. 使用数组代替对象:在某些情况下,使用数组可以比使用对象占用更少的内存。例如,如果你需要存储一系列具有相同结构的数据,使用数组会更高效。假设你要存储多个用户的年龄:
// 使用对象
const users = {
  user1: 20,
  user2: 25,
  user3: 30
}

// 使用数组
const ages = [20, 25, 30]

数组的内存结构相对简单,在存储大量同类型数据时,占用的内存会比对象少。

  1. 避免不必要的嵌套数据结构:深度嵌套的数据结构会增加内存的复杂性和占用量。例如,尽量避免这样的结构:
const complexData = {
  a: {
    b: {
      c: {
        d: '一些数据'
      }
    }
  }
}

如果可以,尽量将其扁平化:

const flatData = {
  a_b_c_d: '一些数据'
}

这样虽然可能会使属性名变得较长,但在内存管理上会更高效。

监控内存使用情况

  1. 使用浏览器开发者工具:现代浏览器的开发者工具都提供了内存分析功能。例如,在 Chrome 浏览器中,可以打开“Performance”面板,录制一个内存快照。然后通过“Memory”面板查看内存使用情况,分析哪些对象占用了大量内存。

  2. 使用第三方工具:一些第三方工具,如 vue - devtools,也可以帮助我们监控 Vue 应用的内存使用情况。vue - devtools 可以显示 Vue 组件的层级结构、数据状态等信息,通过分析这些信息,可以找出可能存在内存问题的组件。

在使用这些工具时,需要注意在不同的操作场景下进行监控,例如在页面加载、用户交互等过程中,观察内存的变化情况,从而准确地定位内存问题。

综合优化案例分析

案例背景

假设我们正在开发一个电商类的 Vue 应用,该应用包含商品列表、商品详情、购物车等功能。随着功能的不断增加,应用的性能出现了问题,页面加载缓慢,内存占用过高。

性能优化步骤

  1. 代码拆分与懒加载
    • 对于商品详情页面,由于其包含较多的图片和详细信息,文件体积较大。我们对商品详情组件进行懒加载。在路由配置中:
const router = new VueRouter({
  routes: [
    {
      path: '/product/:id',
      name: 'ProductDetail',
      component: () => import('./components/ProductDetail.vue')
    }
  ]
})

这样,只有当用户点击商品进入详情页时,才会加载商品详情组件,减少了首页加载时的文件体积。

  • 在商品列表中,有些组件如“推荐商品”组件,只有在特定条件下(如用户登录后)才会显示。我们对其进行懒加载:
<template>
  <div>
    <ul>
      <li v - for="product in products" :key="product.id">{{ product.name }}</li>
    </ul>
    <component :is="recommendedComponent" v - if="isLoggedIn && recommendedComponent"></component>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: [],
      isLoggedIn: false,
      recommendedComponent: null
    }
  },
  methods: {
    async loadRecommendedComponent() {
      const { default: RecommendedProducts } = await import('./components/RecommendedProducts.vue')
      this.recommendedComponent = RecommendedProducts
    }
  },
  mounted() {
    // 模拟用户登录判断
    this.isLoggedIn = true
    if (this.isLoggedIn) {
      this.loadRecommendedComponent()
    }
  }
}
</script>
  1. 优化图片资源
    • 商品图片大多是 JPEG 格式,我们使用 image - webpack - loader 进行压缩。在 webpack.config.js 中配置:
module.exports = {
  module: {
    rules: [
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'file - loader',
            options: {
              name: 'images/[name].[ext]'
            }
          },
          {
            loader: 'image - webpack - loader',
            options: {
              mozjpeg: {
                progressive: true,
                quality: 70
              }
            }
          }
        ]
      }
    ]
  }
}

经过压缩后,图片文件体积明显减小,页面加载速度加快。 3. 优化 CSS

  • 各个组件都使用了 scoped CSS,避免了样式污染。例如,商品列表组件的样式:
<template>
  <div class="product - list">
    <ul>
      <li v - for="product in products" :key="product.id">{{ product.name }}</li>
    </ul>
  </div>
</template>

<style scoped>
.product - list {
  list - style: none;
  padding: 0;
}
.product - list li {
  margin: 10px 0;
}
</style>
  • 同时,在 vue.config.js 中配置了 CSS 压缩:
module.exports = {
  css: {
    minimize: true,
    loaderOptions: {
      css: {},
      postcss: {
        plugins: [
          require('cssnano')({
            preset: 'default'
          })
        ]
      }
    }
  }
}

压缩后的 CSS 文件体积变小,提高了页面加载性能。

内存管理步骤

  1. 避免内存泄漏
    • 在购物车组件中,绑定了一个 windowstorage 事件,用于实时更新购物车数据。在组件销毁时,解绑该事件:
<template>
  <div>
    <ul>
      <li v - for="item in cartItems" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      cartItems: []
    }
  },
  mounted() {
    window.addEventListener('storage', this.updateCart)
  },
  methods: {
    updateCart() {
      // 更新购物车数据逻辑
    },
    beforeDestroy() {
      window.removeEventListener('storage', this.updateCart)
    }
  }
}
</script>
  • 在商品列表组件中,使用了一个定时器来模拟数据的实时更新。在组件销毁时,清除定时器:
<template>
  <div>
    <ul>
      <li v - for="product in products" :key="product.id">{{ product.name }}</li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      products: [],
      timer: null
    }
  },
  mounted() {
    this.timer = setInterval(() => {
      // 模拟数据更新逻辑
      this.products = [...this.products, { name: '新商品' }]
    }, 5000)
  },
  beforeDestroy() {
    clearInterval(this.timer)
  }
}
</script>
  1. 优化数据结构
    • 购物车数据原本使用了一个深度嵌套的对象来存储商品信息,如下:
const cart = {
  user1: {
    products: {
      product1: {
        name: '商品 1',
        price: 100,
        quantity: 2
      },
      product2: {
        name: '商品 2',
        price: 200,
        quantity: 1
      }
    }
  }
}

为了减少内存占用,将其扁平化:

const cart = [
  {
    userId: 'user1',
    productId: 'product1',
    name: '商品 1',
    price: 100,
    quantity: 2
  },
  {
    userId: 'user1',
    productId: 'product2',
    name: '商品 2',
    price: 200,
    quantity: 1
  }
]

通过以上性能优化和内存管理技巧的综合应用,电商应用的性能得到了显著提升,页面加载速度加快,内存占用也保持在合理范围内。

通过对 Vue CLI 性能优化与内存管理技巧的深入探讨和实际案例分析,希望开发者能够在 Vue 项目开发中,灵活运用这些技巧,打造出高性能、低内存占用的优秀前端应用。在实际项目中,还需要根据具体情况不断调整和优化,以满足用户对应用性能的高要求。