前端性能优化:Vue + Vite 全链路性能提升与打包体积压缩指南

1. 加载与渲染优化

1.1. 懒加载(延迟加载)

对于页面中非首屏的图片或者组件,可以采用延迟加载的方式,可以有效减少首屏加载资源数,提升页面初始加载速度。

1.1.1. 路由懒加载

不要一次性加载所有页面的代码,而是按需加载

js 复制代码
const User = () => import('@/views/User.vue')

1.1.2. 组件懒加载

对于首屏不重要的组件,用 defineAsyncComponent 或者异步组件加载

js 复制代码
import { defineAsyncComponent } from 'vue'
const Chart = defineAsyncComponent(() => import('@/components/Chart.vue'))

有时候资源拆分过细也不好,可能造成浏览器 http 请求过多,以下三种场景适合组件懒加载的场景:

  1. 页面 JS 文件体积较大
    页面打开较慢时,可通过组件懒加载拆分资源,让浏览器并行下载,提高首屏加载速度(例如首页)。
  2. 按需加载的组件
    某些组件不是页面初始化就需要的,而是在特定条件下才显示,可使用懒加载(例如弹窗组件)。
  3. 高复用组件
    对于多个页面都会用到的组件,可懒加载进行抽离,一方面利用缓存减少重复下载,另一方面减小每个页面的 JS 文件体积(例如表格组件、图表组件)。

1.2. 预加载与预连接

对于关键的资源,可以使用 preload 来提示浏览器尽快下载。对于需要连接的第三方域名,可以使用 preconnectdns-prefetch 来提前建立连接,减少后续请求的等待时间。

下面是图片预加载示例

在打包或开发时,自动扫描指定目录下的图片文件,并在 HTML 中注入 <link rel="prefetch" href="...">,实现预加载图片,提高页面性能。

js 复制代码
import fg from "fast-glob";

// vite 使用
export const preloadImages = options => {
  return {
    name: "vite-plugin-preload-images",
    transformIndexHtml(html, ctx) {
      // console.log(html);
      // return html.replace("<title>Vite App</title>", "<title>Hello word</title>");
      const {dir, attrs} = options;

      const files = fg.sync(dir, {
        cwd: ctx.server?.config.publicDir,
      });

      const images = files.map(file => ctx.server?.config.base + file);

      return images.map(herf => {
        return {
          tag: "link",
          attrs: {
            rel: "prefetch", // preload
            href: herf,
            as: "image",
            ...attrs,
          },
        };
      });
    },
  };
};
js 复制代码
// vite.config.js
import {fileURLToPath, URL} from "node:url";
import {defineConfig} from "vite";
import vue from "@vitejs/plugin-vue";
import {resolve} from "path";
import vueJsx from "@vitejs/plugin-vue-jsx";
import {preloadImages} from "./src/utils/preloadImages";

export default defineConfig({
  plugins: [
    vue(),
    vueJsx(),
    preloadImages({
      dir: "images/**/*.{png,jpg,jpeg,webp,gif,svg}",
      attrs: {
        rel: "preload",
      },
    }),
  ],
  resolve: {
    alias: {
      // "@": resolve(__dirname, "src"),
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});
html 复制代码
<template>
	<div class=''>
		<h1>图片预加载</h1>
		<img src="http://localhost:5173/images/image1.png" alt="">
		<img src="http://localhost:5173/images/image2.png" alt="">
		<img src="http://localhost:5173/images/image3.png" alt="">
		<img src="http://localhost:5173/images/image4.png" alt="">
		<img src="http://localhost:5173/images/image5.png" alt="">
		<img src="http://localhost:5173/images/image6.png" alt="">
		<img src="http://localhost:5173/images/image7.png" alt="">
		<img src="http://localhost:5173/images/image8.png" alt="">
		<img src="http://localhost:5173/images/image9.png" alt="">
	</div>
</template>

1.3. 优化关键渲染路径

  • CSS 置顶,JS 置底:将 CSS 标签放在 中,以便浏览器能尽早开始构建 CSSOM(CSS 对象模型)。将 JavaScript 结束标签前,以避免 JavaScript 的加载和执行阻塞 HTML 的解析和渲染。
  • 避免渲染阻塞资源:对于非关键的 CSS 和 JavaScript,可以使用 async 或 defer 属性进行异步加载,避免阻塞页面的渲染。

1.4. 服务端渲染(SSR)或预渲染

对于需要关注 SEO 和首屏加载速度的应用,可以采用服务端渲染的方式。SSR 在服务端将页面渲染成 HTML 字符穿串,然后发给浏览器,使得浏览器可以更快的显示页面内容,对于静态页面,也可以在构建时进行预渲染,生成静态 HTML 文件。

2. Tree Shaking

2.1. 什么是 Tree Shaking?

你可以把 Tree Shaking 理解成:

在打包时,自动把"用不到的代码"像树叶一样抖掉,只保留真正用到的部分。

名字来源于"摇树掉叶子"------想象你的代码像一棵大树,树枝是模块,叶子是函数/变量,没用的叶子会被打包工具摇下来丢掉。

在 Vue 项目里它的作用是:

  • 减少打包体积(加载更快)
  • 去掉无用的代码(提升性能)
  • 按需引入组件/方法

2.2. Vue 中 Tree Shaking 的核心思路

在 Vue + Vite/Webpack 环境中,Tree Shaking 一般基于 ES Model(ESM) 特性来做:

js 复制代码
// ESM 语法
import { ref, reactive } from 'vue';

编译的时候打包工具能分析出哪些 import 的内容没有用到,然后移除没用的部分。

例子:

js 复制代码
import { ref, reactive } from 'vue'

const count = ref(0) // reactive 没用到,所以会被移除

打包后的代码里,只会保留 ref 相关代码。

2.3. 新手常见错误(Tree Shaking 失效)

  1. 使用 CommonJS(require)
js 复制代码
const vue = require('vue') // 无法 Tree Shaking

要用 ES Module:

js 复制代码
import { ref } from 'vue' 
  1. 引入整个库
js 复制代码
import _ from 'lodash' // 会把整个 lodash 都打进去

按需引入:

js 复制代码
import debounce from 'lodash/debounce' 
  1. 在动态代码里用 import
    如果写了动态 require/import,打包工具就没法静态分析哪些代码没用。

2.4. Vue 中如何让 Tree Shaking 更高效

  1. 按需引入组件库(特别是 UI 库)
  2. 不要用 import * as, 这样会引入所有导出内容,Tree Shaking 不好用。
  3. 用 Vite/Webpack 默认的 ES Model,不要手动切成 CommonJS。

3. 打包优化

3.1. 开启代码压缩 (Terser、Esbuild)

  1. 减小代码体积:删除多余的空格、换行、注释,缩短变量名与函数名,去掉调试信息
  2. 简单说:压缩就是让生产环境的代码更小、更快、更安全。

Vite 在 mode: 'production' 打包时,默认就会启用压缩(使用 esbuild )。

所以即使什么都不配,也会自动压缩。也可以手动配置

  1. 使用 Esbuild 压缩(默认,速度快)
js 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    minify: 'esbuild', // 默认就是 esbuild
    target: 'esnext',  // 目标语法,可以是 'es2015'
  }
})
  1. 使用 Terser 压缩(压缩率更高,可配置更多)
bash 复制代码
npm install terser -D
js 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    minify: 'terser', // 启用 Terser
    terserOptions: {
      compress: {
        drop_console: true,   // 删除 console.log
        drop_debugger: true,  // 删除 debugger
      },
      format: {
        comments: false,      // 删除注释
      }
    }
  }
})

3.2. 开启 gzip 或 brotil 压缩(nginx 配置 gzip on)

  1. 开启 gzip/brotli 压缩就是为了让网络传输更快、更省流量,同时提升用户体验。
  2. 当开启 gzip 的时候, 服务器会生成 .gz 文件 ,浏览器访问服务器会返回压缩内容,浏览器负责解压,用户无感知,体积小传输快, 从而提升页面加载速度和降低带宽消耗。
js 复制代码
import viteCompression from "vite-plugin-compression";
js 复制代码
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import viteCompression from 'vite-plugin-compression'  // ✅ 这里要引入

export default defineConfig({
  plugins: [
    vue(),
    viteCompression({   // 开启gzip
      verbose: true,
      disable: false,
      threshold: 10240,
      algorithm: 'gzip',
      ext: '.gz',
    }),
    viteCompression({   // 开启brotil 
      algorithm: 'brotliCompress',
      ext: '.br',
    }),
  ],
})

我这里使用 docker 部署,需要配置 nginx.conf + Dockerfile

nginx 复制代码
server {
  listen 80;
  server_name localhost;

  root /usr/share/nginx/html;
  index index.html;

  # 前端路由 history 模式需要重定向
  location / {
    try_files $uri $uri/ /index.html;
  }

  # 开启 gzip
  gzip on;
  gzip_min_length 1k;
  gzip_comp_level 6;
  gzip_types text/plain text/css application/javascript application/json application/xml+rss image/svg+xml;
  gzip_vary on;

  # 如果打包时生成了 .br 文件,可以启用 brotli
  # 需要 nginx 编译了 brotli 模块才行
  brotli on;
  brotli_comp_level 6;
  brotli_types text/plain text/css application/javascript application/json image/svg+xml;

  # 缓存策略
  location ~* .(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot)$ {
    expires 30d;
    add_header Cache-Control "public";
  }

  # HTML 不缓存,避免版本不更新
  location ~* .(html)$ {
    expires -1;
    add_header Cache-Control "no-cache";
  }
}
Dockerfile 复制代码
# 使用官方 nginx 镜像作为基础镜像
FROM nginx:alpine

# 删除默认配置
RUN rm /etc/nginx/conf.d/default.conf

# 拷贝自定义配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 拷贝构建后的前端资源
COPY dist/ /usr/share/nginx/html/

# 暴露端口
EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

然后把 dist、Dockerfile、nginx.conf 传到服务器进行构建,然后看到 gzip 代表成功了。

4. 缓存策略

合理利用缓存可以极大地提升二次访问的速度。

4.1. 浏览器缓存

通过设置合理的 HTPP 缓存头(如 Cache-ControlExpires, ETagLast-Modified),可以让浏览器缓存静态资源。当用户再次访问时,浏览器可以直接从本地缓存中读取,无需再次向服务器请求。

4.2. 使用 CDN 加载第三方库(Vue、ECharts、Lodash 等)

  1. 减少项目打包体积:第三方代码不再打包进代码,打包后的 js 文件更小,首屏加载更快。
  2. CDN 节点分布全球离用户更近下载更快,CDN 服务器一般宽带大性能好,支持高并发访问。
  3. 用户可能访问过其他使用相同的 CDN 网站,浏览器已经缓存了这些库不需要再次下载。
  4. 第三方库从 CDN 下载,不占用自己服务器宽带,服务器只需要提供自家的业务代码。

4.3. Ajax 缓存

对于一些不长变化的 Ajax 请求,可以将结果缓存起来,当再次发起相同的请求时,可以直接从缓存中读取数据,减少服务器压力,提升响应速度。

5. 资源优化

5.1. 文件优化

文件压缩与合并:通过对 CSS、JavaScript 等文件进行压缩,可以有效减小文件体积。将多个小文件合成一个大文件,减少 HTPP 请求次数,从而降低网络延迟。

5.2. 图片优化

  1. 使用懒加载(loading="lazy" 或 Vue 指令)
  2. 用雪碧图减少请求数量
  3. 图片转 base64 格式

6. 分片更新(避免一次渲染太多 DOM)------ 减少重(Repaint)绘与重排(Reflow)

DOM 操作和样式的改变可能会引发浏览器的重绘与重排,这是非常消耗性能的操作。应尽量减少不必要的 DOM 操作,比如:批量修改 DOM 元素、批量修改样式、或者使用 CSS transform 等不会触发回流的属性来实现动画效果。。

6.1. 使用 requestAnimationFrame

requestAnimationFrame 是浏览器提供的一个 API,用来告诉浏览器:我想要执行一个动画,请在下次重绘(repaint)之前调用指定的函数来更新动画

它的本质是一个 动画调度器,让动画和显示器的刷新率(一般是 60Hz,也就是 1 秒 60 帧)保持同步。

js 复制代码
let id = requestAnimationFrame(callback)

// 取消动画
cancelAnimationFrame(id)
  • callback:你定义的函数,浏览器会在下一次绘制前调用它,并把一个 时间戳(DOMHighResTimeStamp) 传进去。
  • id:返回一个 ID,可以用来取消动画。

setTimeout / setInterval / requestAnimationFrame 的区别:

  • setTimeout是延时执行一次代码,到期后把回调函数丢进事件队列,排队执行(受 CPU 负载和事件队列影响,不精确 )。就像闹钟,设定好时间,到时间了就提醒一次,常用在"延时执行一次任务"。
  • setInterval是定时循环执行代码,按设定的时间间隔丢进队列(同上,不精确,可能堆积)。就像时钟,每隔一段时间滴答一次,不管上次有没有执行完,都会继续安排下次,容易出现堆积卡顿,所以不太适合做动画。
  • requestAnimationFrame是浏览器专门给动画准备的,它会在屏幕每次刷新之前(通常是 16.7ms 一次,60帧/秒) 自动调用函数,既保证了动画流畅, 又能在后台标签页自动暂停,节省性能(上面的两个仍然执行但是会降频)。

6.2. 虚拟滚动(比如 vue-virtual-scroller)来优化长列表

想象一下,如果有 10000 条数据需要展示,如果用最常规的方法,比如 Vue 中的 v-for 会发生什么?

html 复制代码
<ul>
  <li v-for="friend in friendsList" :key="friend.id">
    {{ friend.name }}
  </li>
</ul>

浏览器会一次性地、真实地创建 10000 个 <li> 元素并把它们全部渲染到 DOM (文档对象模型) 中。这会带来一系列严重的性能问题:

  1. DOM 元素过多:浏览器需要管理海量的 DOM 节点,这会消耗大量内存。
  2. 渲染时间过长:首次渲染页面时,需要创建和绘制所有 10,000 个元素,导致页面长时间白屏,用户体验极差。
  3. 滚动卡顿:当用户滚动这个长列表时,浏览器需要计算成千上万个元素的位置和样式,这会占用大量 CPU,导致滚动动画掉帧,感觉非常卡顿。

核心问题:用户的屏幕(视口)一次只能看到大约 10-20 个列表项,但却渲染了 10,000 个,造成了巨大的性能浪费。

那么这种情况就非常适合虚拟列表了!

虚拟列表是一种 性能优化技术,用于渲染超长列表。只渲染可视区域内的元素,以及上下预留的一小部分,其他看不见的元素不会渲染,随着用户的滚动,旧的 DOM 被复用,新进入的视野才会生成,这样无论数据有上千条还是上万条,页面上始终基友十几个 DOM 节点,大幅度降低内存和渲染压力。

推荐几个基于框架的开源实现:

  • 基于React的 react-virtualized
  • 基于Vue 的 vue-virtual-scroll-list
  • 基于Angular的 ngx-virtual-scroller
html 复制代码
<template>
  <div class="test-container">
    <h1>虚拟滚动测试</h1>
    <p>正在渲染 {{ listData.length }} 条数据</p>

    <RecycleScroller class="scroller" :items="listData" :item-size="60" key-field="id" v-slot="{ item }">
      <!-- 这是一个标准的、不会出错的列表项模板 -->
      <div class="list-item">
        <div class="item-id">ID: {{ item.id }}</div>
        <div class="item-content">
          <h3>{{ item.name }}</h3>
          <p>{{ item.content }}</p>
        </div>
      </div>
    </RecycleScroller>
  </div>
</template>

<script setup>
  import { ref } from 'vue';
  // 关键:确保组件是从新安装的包中局部导入的
  import { RecycleScroller } from 'vue-virtual-scroller';

  // 生成测试数据
  const listData = ref(
    Array.from(Array(10000).keys()).map(i => ({
      id: i + 1,
      name: `项目 ${i + 1}`,
      content: `这是第 ${i + 1} 个列表项的详细内容。`
    }))
  );
</script>

<style>
  @import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';

  .test-container {
    padding: 20px;
    border: 2px dashed #42b983;
    margin: 20px;
  }

  /* 必须为滚动容器指定一个固定的高度 */
  .scroller {
    height: 500px;
    border: 1px solid #ccc;
    border-radius: 4px;
  }

  /* 列表项的样式,确保布局不会出错 */
  .list-item {
    height: 60px;
    /* 必须和 :item-size 属性值保持一致 */
    padding: 0 15px;
    border-bottom: 1px solid #eee;
    display: flex;
    align-items: center;
    /* 垂直居中 */
  }

  .item-id {
    font-weight: bold;
    color: #888;
    margin-right: 15px;
  }

  .item-content {
    text-align: left;
  }

  .item-content h3,
  .item-content p {
    margin: 0;
    padding: 0;
  }
</style>
相关推荐
富婆苗子几秒前
关于wangeditor的自定义组件和元素
前端·javascript
顾辰逸you2 分钟前
uniapp--咸虾米壁纸(三)
前端·微信小程序
北鸟南游6 分钟前
用现有bootstrap的模板,改造成nuxt3项目
前端·bootstrap·nuxt.js
前端老鹰8 分钟前
JavaScript Intl.RelativeTimeFormat:自动生成 “3 分钟前” 的国际化工具
前端·javascript
梦想CAD控件8 分钟前
(在线CAD插件)网页CAD实现图纸表格智能提取
前端·javascript·全栈
木子雨廷25 分钟前
Flutter 开发一个plugin
前端·flutter
重生之我是一名前端程序员28 分钟前
websocket + xterm 前端实现网页版终端
前端·websocket
sorryhc29 分钟前
【AI解读源码系列】ant design mobile——Space间距
前端·javascript·react.js
uhakadotcom44 分钟前
NPM与NPX的区别是什么?
前端·面试·github
GAMC1 小时前
如何修改node_modules的组件不被install替换?可以使用patch-package
前端