👍终极指南:全面优化Vue项目性能与效率
前言
随着 Vue.js 的广泛应用,性能优化问题也日益凸显。进行性能优化的主要目的是为了提升用户体验。在开发过程中,我查阅了许多关于 Vue 项目优化 的相关文档,并进行了实际的应用与实践。本文将分享我在这一过程中积累的一些心得与体会。
以下内容将主要围绕以下几个问题进行讨论:
- 如何提高页面加载速度?
- 如何优化交互响应速度?
- 如何解决页面卡顿情况?
雅虎军规
雅虎军规是众所周知的性能优化指南,我们可以从这些规则开始讨论 雅虎军规
站在巨人的肩膀上
接下来,我们将系统地梳理整个性能优化方案,从浏览器渲染页面的完整过程出发。 我们将重点讨论以下几个方���:
- 请求响应
- 渲染
- 资源加载
- 图片
- 构建
请求响应
- DNS解析
- HTTP 长连接与 HTTP/2
- CDN(内容分发网络)
- 数据资源压缩传输
- 缓存策略
DNS解析
当浏览器请求资源时,需要进行 DNS 解析。在优化方案中,我们可以通过减少 DNS 解析次数来提升性能,同时在解析的时机上进行 DNS 预解析。
DNS 优化可以采取两种主要方式:
- 进行 DNS 预解析
- 减少 DNS 请求次数(即减少 HTTP 请求)
首先,我们来看看 DNS 预获取的概念。
例如,在百度首页,我们可以看到 DNS 预获取的应用。当浏览器遇到
dns-prefetch
指令时,它会提前解析指定的域名,这样在后续遇到这些域名的请求时,就能有效减少 DNS 解析所需的时间。
HTTP 长链接和 HTTP/2
在众所周知的 HTTP 协议中,传统的 HTTP 连接需要进行三次握手才能完成一次 HTTP 请求。随着新的 HTTP/1.1 协议的出现,引入了多路复用功能(即 Connection: keep-alive),使得客户端和服务端可以复用连接,减少了沟通时间。
通过启用 HTTP/2 协议,例如在 Nginx 中开启 HTTP/2,可以进一步优化 HTTP 请求的性能。HTTP/2 支持请求的并行处理、头部压缩、服务器推送等功能,提高了网络传输效率,使网站加载速度更快、性能更优。
CDN
内容分发网络(CDN)是由分布在多个不同地理位置的 Web 服务器组成的网络。我们都知道,服务器距离用户越远,延迟就越高。
CDN 的出现正是为了解决这一问题,通过在多个位置部署服务器,使用户能够更接近服务器,从而缩短请求时间。此外,CDN 还可以减少 DNS 解析时间。由于浏览器对同一域名下的并发请求数量有限制,使用 CDN 可以有效增加同一时间内的并发请求数量,从而进一步提升网站的性能和加载速度。``
压缩传输数据资源
在网络传输中,压缩数据资源是一种常见的优化手段,可以显著减少请求大小,提升加载速度。
- Cookie优化:浏览器请求服务端接口时会自动携带当前域下的Cookie。为了减少无用的Cookie,可以有效减小请求大小,提高网络传输效率。
- Nginx开启Gzip:通过开启Gzip压缩功能,Nginx可以大幅压缩项目文件的大小,减少网络传输时间。这样的优化措施可以有效提升网站的性能和用户体验。
注:Gzip压缩对于静态文件(如CSS、JavaScript和HTML)效果显著,而对于图片等二进制文件效果并不明显,因此对这类文件不需要进行Gzip压缩
缓存
使用有效的缓存策略可以更高效地加载资源,优化网站性能。 浏览器缓存机制:强缓存、协商缓存
渲染
渲染过程是浏览器从获取 HTML 到最终在屏幕上显示内容所经历的关键步骤:
- 解析 HTML 标记并构建 DOM 树
- 解析 CSS 标记并构建 CSSOM 树
- 将 DOM 树和 CSSOM 树合并成一个渲染树(render tree)
- 根据渲染树进行布局计算,确定每个节点的几何信息
- 将各个节点绘制到屏幕上,完成页面的渲染。
这一系列步骤是浏览器将网页内容呈现给用户的关键过程,每一步都直接影响着页面加载速度和用户体验。
首屏渲染优化
- 使用骨架屏进行预渲染:骨架屏是一种有效的用户体验优化技术,能够在内容加载之前展示页面的基本结构,从而提升用户的视觉感受。
- 首屏渲染优先:在首屏渲染时,优先加载与用户立即可见的内容,而将与之无关的资源在用户滑动后再进行加载,以提升页面的响应速度。
- 服务端渲染:通过服务端渲染(SSR),可以减少页面的二次请求和渲染耗时,从而加快用户看到内容的速度。
- 资源预请求与预加载:利用资源预请求(preconnect)和预加载(preload)技术,可以提前加载关键资源,确保首屏渲染更加顺畅,进一步提升用户体验。
这些优化策略有助于提升首屏加载速度,使用户能够更快地访问和体验内容。
长列表优化
在处理长列表时,通常采用虚拟渲染技术,根据滚动距离来渲染特定的列表数据,以优化渲染效率。 这种优化方法可以显著提升长列表的性能,减少页面渲染开销,提高用户体验
资源加载策略优化
在项目开发中,优化资源加载顺序和效率对于提升用户体验至关重要。以下是一些常见的优化策略:
- 懒加载:也称为按需加载,根据用户行为或需求动态加载资源,如路由懒加载和组件懒加载等。
- 预加载:在页面空闲时预先加载可能需要的资源,提升后续操作的响应速度。
举个🍐,考虑以下项目加载顺序:
- 登录
- 整体页面框架
- 顶部菜单栏
- 左侧工具栏
- 底部状态栏
- 文件目录栏
- 文件详情
- 内容展示
- 编辑功能
- 菜单功能
- 搜索功能
- 插件功能
- 登录页:默认加载的起始页,提供基本的用户登录功能。
- 整体页面框架:登录后加载,包括顶部菜单栏、左侧工具栏和底部状态栏等共用的页面元素。
- 通用功能页:预加载常用页面资源,如顶部菜单栏、左侧工具栏和底部状态栏等。
- 具体功能页面:根据用户需要按需加载,如文件目录栏、文件详情页及其内容展示、编辑功能和菜单功能等。
- 插件功能:在核心功能加载完成后,根据用户安装的插件需求动态加载相应的功能模块。
通过合理的加载顺序和优化策略,可以有效提升页面加载速度和用户体验,确保用户能快速访问和使用所需的功能。
图片加载优化策略
在网页设计中,优化图片加载不仅能提升页面性能,还能改善用户体验。以下是几种常见的图片加载优化策略:
-
图片懒加载:可以通过自定义指令或封装统一的组件来实现通用的图片懒加载功能。这种方式适用于多图页面,有效地减少初始加载的资源,从而提升页面加载速度。
-
缩略图:缩略图是一种优化图片资源的有效方式,特别是在展示的图片大小与实际图片尺寸差异较大时。常见的对象存储服务(如阿里云和腾讯云)都支持图片的缩略展示,能够快速加载小尺寸的预览图。
[如何缩放图片_对象存储(OSS)-阿里云帮助中心](https://help.aliyun.com/document_detail/44688.html)
-
雪碧图:雪碧图是一种将多个图标合并为一张图片的技术,通过定位方式加载图标,显著减少HTTP请求数。这种方法适用于图标收集和小图标的使用场景。
-
Base64编码:将小图片转为Base64格式,可以减少HTTP请求次数,适合用于小图标或很少更新的图片资源。不过,要注意,Base64编码会增加HTML文件的大小,适合小型图片。
-
WebP格式图片:WebP是一种新兴的图片格式,具有相对较小的文件体积和更快的加载效率。然而,其缺点是兼容性不足,部分浏览器可能不支持这种格式。
构建优化策略
在考虑打包体积和整体构建速度时,我们可以从以下几个方面来进行优化:
打包体积和构建速度的影响
打包体积直接影响了线上加载速度和用户体验;构建速度则直接影响了日常开发效率。
优化工具
在优化过程中,可以使用一些工具和策略来帮助分析和优化:
speed-measure-webpack-plugin
:这是一个用于测量和分析Webpack打包时间的插件,能够帮助开发者找出造成构建速度慢的原因,并进行相应优化。webpack-bundle-analyzer
:用于分析Webpack打包生成的各个模块的大小,帮助优化打包体积。
打包时间优化
在日常打包过程中,需要考虑到开发构建和生产构建的不同需求:
- 开发构建:在开发阶段,主要关注构建速度的快速反馈和热更新功能,因此可以暂时关闭代码混淆压缩和图片压缩等耗时操作,保持开发效率。
- 生产构建:在生产打包阶段,尽管构建速度依然重要,但必须开启代码和图片的压缩等优化功能,以保证最终项目的体积尽可能小,并保持较快的构建速度。
打包时间优化方式:
- 传统优化方式基于
**Webpack**
:包括优化loader和plugin配置,使用Tree Shaking、代码分割(SplitChunksPlugin)、懒加载等技术来减少不必要的代码和模块加载。 - 分模块构建:将大型项目拆分为多个模块进行独立构建,以提高并行度和整体构建速度。
- 基于Vite的构建工具切换:Vite基于ES模块的快速开发服务器和构建工具,能够显著提升开发体验和构建速度,特别适合开发阶段使用。
- 基于esbuild插件的构建效率优化:esbuild是一个基于Go语言构建的快速JavaScript和TypeScript编译器,通过其插件可以加速项目的构建过程。
基于 Webpack 的传统优化方式
Webpack的构建流程中,主要耗时的环节是递归遍历各个入口文件并处理依赖,经过loader的转化处理。针对这一过程,我们可以采取以下传统优化方式:
缓存优化
cache-loader
:在性能开销较大的loader之前添加cache-loader,将处理结果缓存起来,减少重复处理时间。HardSourceWebpackPlugin
:提供模块中间缓存,可显著加快二次构建速度,将缓存存放在默认路径node_modules/.cache/hard-source。babel-loader的cacheDirectory
:配置babel-loader的cacheDirectory参数,将编译结果缓存,加速构建过程。
多进程优化
HappyPack
(不再维护):通过多进程处理loader,提高构建速度,但已不再维护。Thread-loader
:适用于webpack,通过创建worker池处理loader,提高构建速度。尽管存在兼容性限制,但在一些情况下仍然是有效的优化方式。
寻址优化
- Loader设置include、exclude:通过指定loader的include和exclude选项,限定loader处理的路径范围,避免不必要的递归处理,提高构建效率。
分模块构建优化策略
在Webpack构建流程中,逐步处理入口模块和依赖模块的过程会消耗大量时间,影响整体构建速度。针对这一问题,可以采取分模块构建优化策略,即只构建必要的模块,以提升构建效率。以下是一种实现分模块构建的优化方案:
分模块构建方案
- 路由模块定义:定义各个模块的路由组件,如moduleA、moduleB等。
- 路由配置:根据需要构建的模块,动态生成对应的路由配置文件(routesConfig)。
- 前置脚本:编写前置命令行脚本,在构建前根据用户选择的模块生成新的routesConfig文件。
- 模板文件:借助模板引擎(如EJS)生成新的路由配置文件(dev.routerConfig.ts)。
- 交互式命令:通过交互式命令行工具(如inquirer)让用户选择需要构建的模块列表。
- 动态生成路由:根据用户选择的模块,动态生成新的路由配置文件,包含选择的模块路由信息。
随着项目体量的不断增大,构建过程中耗时的主要环节集中在第7步,即递归遍历抽象语法树(AST)并解析require
语句。这一过程会反复进行,直到完整遍历整个项目。然而,在日常开发中,我们通常只会关注单独的模块,其他模块在此时并不需要被加载和构建。因此,在依赖收集阶段,如果能够仅构建必要的模块,将显著减少整体构建时间。这正是我们所要实现的目标——分模块构建。
假设我们的项目共有6个模块,每个模块的平均构建时间为3秒,那么整体构建时间将达到18秒。然而,如果我们仅构建所需的模块,构建时间将缩短至3秒。
为此,我们可以通过一个前置命令行脚本来收集本次启动所需的模块,并根据需求动态生成相应的
routesConfig
配置文件。以下是路由代码的一个大致示例:
import Vue from 'vue';
import VueRouter, { Route } from 'vue-router';
// 1. 定义路由组件.
// 这里简化下模型,实际项目中肯定是一个一个的大路由模块,从其他文件导入
const moduleA = { template: '<div>AAAA</div>' }
const moduleB = { template: '<div>BBBB</div>' }
const moduleC = { template: '<div>CCCC</div>' }
const moduleD = { template: '<div>DDDD</div>' }
const moduleE = { template: '<div>EEEE</div>' }
const moduleF = { template: '<div>FFFF</div>' }
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
// 我们后面再讨论嵌套路由。
const routesConfig = [
{ path: '/A', component: moduleA },
{ path: '/B', component: moduleB },
{ path: '/C', component: moduleC },
{ path: '/D', component: moduleD },
{ path: '/E', component: moduleE },
{ path: '/F', component: moduleF }
]
const router = new VueRouter({
mode: 'history',
routes: routesConfig,
});
// 让路由生效 ...
const app = Vue.createApp({})
app.use(router)
使用NormalModuleReplacementPlugin
生成新的路由配置文件,然后通过交互式命令,打包特定模块
// vue.config.js
if (process.env.NODE_ENV === 'development') {
//开发环境
config.plugins.push(new webpack.NormalModuleReplacementPlugin(
/src\/router\/config.ts/,
'../../dev.routerConfig.ts'
)
)
}
上面的代码的功能是将实际使用的 config.ts
文件替换为自定义配置的 dev.routerConfig.ts
文件。那么,dev.routerConfig.ts
文件的内容又是如何生成的呢?实际上,我们借助了 inquirer
和 EJS 模板引擎,通过一个交互式的命令行问答,选择需要的模块,并基于用户的选择动态生成新的 dev.routerConfig.ts
代码。
为此,我们需要对启动脚本进行改造,在执行 vue-cli-service serve
之前,先运行一段我们的前置脚本。
{
// ...
"scripts": {
- "dev": "vue-cli-service serve",
+ "dev": "node ./script/dev-server.js && vue-cli-service serve",
},
// ...
}
而 dev-server.js
所需要做的事,就是通过inquirer
实现一个交互式命令,用户选择本次需要启动的模块列表,通过 ejs
生成一份新的 dev.routerConfig.ts
文件
// dev-server.js
const ejs = require('ejs');
const fs = require('fs');
const child_process = require('child_process');
const inquirer = require('inquirer');
const path = require('path');
const moduleConfig = [
'moduleA',
'moduleB',
'moduleC',
// 实际业务中的所有模块
]
//选中的模块
const chooseModules = [
'home'
]
function deelRouteName(name) {
const index = name.search(/[A-Z]/g);
const preRoute = '' + path.resolve(__dirname, '../src/router/modules/') + '/';
if (![0, -1].includes(index)) {
return preRoute + (name.slice(0, index) + '-' + name.slice(index)).toLowerCase();
}
return preRoute + name.toLowerCase();;
}
function init() {
let entryDir = process.argv.slice(2);
entryDir = [...new Set(entryDir)];
if (entryDir && entryDir.length > 0) {
for(const item of entryDir){
if(moduleConfig.includes(item)){
chooseModules.push(item);
}
}
console.log('output: ', chooseModules);
runDEV();
} else {
promptModule();
}
}
const getContenTemplate = async () => {
const html = await ejs.renderFile(path.resolve(__dirname, 'router.config.template.ejs'), { chooseModules, deelRouteName }, {async: true});
fs.writeFileSync(path.resolve(__dirname, '../dev.routerConfig.ts'), html);
};
function promptModule() {
inquirer.prompt({
type: 'checkbox',
name: 'modules',
message: '请选择启动的模块, 点击上下键选择, 按空格键确认(可以多选), 回车运行。注意: 直接敲击回车会全量编译, 速度较慢。',
pageSize: 15,
choices: moduleConfig.map((item) => {
return {
name: item,
value: item,
}
})
}).then((answers) => {
if(answers.modules.length===0){
chooseModules.push(...moduleConfig)
}else{
chooseModules.push(...answers.modules)
}
runDEV();
});
}
init();
模板代码的简单示意:
// 模板代码示意,router.config.template.ejs
import { RouteConfig } from 'vue-router';
<% chooseModules.forEach(function(item){%>
import <%=item %> from '<%=deelRouteName(item) %>';
<% }) %>
let routesConfig: Array<RouteConfig> = [];
/* eslint-disable */
routesConfig = [
<% chooseModules.forEach(function(item){%>
<%=item %>,
<% }) %>
]
export default routesConfig;
dev-server.js
的核心在于启动一个 inquirer 交互命令行服务,让用户选择需要构建的模块,类似于这样:
模板代码示例中,
router.config.template.ejs
是一个 EJS 模板文件,而 chooseModules
则是我们在终端输入时获取到的用户选择的模块集合数组。根据这个选择列表,我们可以生成新的 routesConfig
文件。
这样,我们就实现了**分模块构建 **
基于Vite的构建工具切换
Vite是一个基于浏览器原生ES模块的开发服务器,利用浏览器解析imports,并在服务器端按需编译返回,完全摒弃了传统的打包概念,实现随启随用的即时构建。Vite不仅支持Vue文件,还实现了高效的热更新,即使模块增多,热更新速度也不会变慢。 Vite之所以如此快速,主要体现在以下两个方面:
- 项目冷启动更快:Vite在项目启动时的冷启动速度非常快,几乎可以立即启动开发服务器,无需长时间等待。
- 热更新更快:Vite实现了高效的热更新机制,使得在开发过程中的代码修改能够快速反映在浏览器中,即使模块增多,热更新速度也保持高效。
相比之下,传统的Webpack构建过程相对繁琐,需要逐步处理模块依赖关系,而Vite则通过浏览器端实现了即时编译和模块加载,大大提升了开发效率。Vite目前主要适用于开发阶段替代Webpack,并且在开发体验和速度方面有明显优势。
前面已经提到,Webpack在启动时会从入口文件开始,调用所有配置的Loader对模块进行编译,然后找出该模块依赖的模块,递归这个过程,直到所有入口依赖的文件都经过了处理。这个阶段是非常耗时的。
现在我们来看看Vite:
Vite通过在一开始将应用中的模块区分为"依赖"和"源码"两类,从而改进了开发服务器的启动时间。Vite的高效之处主要体现在以下两点:
- 使用Go语言进行依赖预构建:Vite利用Go语言的多进程特性,采用esbuild进行依赖预构建。esbuild是使用Go编写的打包工具,比基于JavaScript的打包器预构建依赖快10-100倍。依赖预构建的主要目的是:
- 在开发阶段,Vite的开发服务器将所有代码视为原生ES模块。因此,Vite需要先将CommonJS或UMD发布的依赖项转换为ESM。
- Vite将内部模块的ESM依赖关系转换为单个模块,以提高后续页面加载性能。通过这种优化,可以减少请求次数和提升加载速度。
- 按需编译返回:Vite以原生ESM方式提供源码,让浏览器接管了打包程序的部分工作。Vite只需要在浏览器请求源码时进行转换并按需提供源码。根据实际情况动态导入代码,只有在当前屏幕上实际使用时才会被处理。
通过以上优化措施,Vite实现了高效的开发构建流程,提升了开发体验和构建速度。接下来,我们将从项目改造的角度开始优化。 我们的项目背景是基于Vue-cli 4的Vue2项目,改造为Vite的步骤主要包括:
- 安装Vite
- 配置index.html(使Vite能够解析
<script type="module" src="...">
标签指向源码) - 配置vite.config.js
- 在package.json的scripts模块下增加启动命令"vite": “vite”
当通过命令行运行npm run vite
时,Vite会自动解析项目根目录下名为vite.config.js的文件,读取相应配置。对于vite.config.js的配置,通常比较简单:
- Vite提供了对.scss、.sass、.less和.stylus文件的内置支持
- 具备天然对TypeScript的支持,开箱即用
- 对基于Vue2的项目提供支持。虽然可能会遇到不同问题,但根据报错逐步调试即可,例如通过官方插件兼容.tsx和.jsx文件。
在改造源码时,我们遇到了一些小问题:
- 在.tsx中使用装饰器可能导致编译问题,我们通过修改@vitejs/plugin-vue-jsx来支持Vue2下的jsx语法
- 由于Vite仅支持ESM语法,需要将代码中的模块引入方式由require改为import
- Sass预处理器无法正确解析样式中的/deep/,可以使用::v-deep进行替换
- 其他一些小问题,例如Webpack环境变量的兼容性,SVG图标的兼容性
经过改造为Vite后,开发构建的时间缩短至了2.6秒。
总结
在前端页面加载资源时,主要涉及到js和css文件。由于浏览器在同一域名下的最大请求并发量为6,因此资源大小和请求数量会显著影响页面加载性能。以下是优化方案的要点:
- Webpack优化:通过合理配置Webpack,优化构建过程,提升打包效率和资源管理能力。
- 代码拆分:使用Webpack的代码拆分功能,将大文件拆分为较小的片段,实现按需加载,减少首次加载时间。
- 代码压缩:使用工具如UglifyPlugin、MiniCssExtractPlugin和HtmlWebpackPlugin对JavaScript、CSS和HTML进行压缩,减少文件大小,加快加载速度。
- 持久化缓存:通过Webpack的文件指纹(chunkhash)和配置合适的缓存头部,使浏览器能够缓存资源并有效管理缓存。
- 监测与分析:使用工具(如Webpack Bundle Analyzer)分析打包后的文件大小和依赖关系,识别和解决潜在的性能瓶颈。
- 按需加载:使用Webpack或其他模块打包工具实现按需加载,根据页面需求动态加载资源,减少首次加载时间和带宽消耗。
针对静态资源的优化措施包括:
- 压缩静态资源:使用UglifyPlugin(JavaScript)、MiniCssExtractPlugin(CSS)和HtmlWebpackPlugin(HTML)等工具对静态资源进行压缩,减少文件大小。
JavaScript:UglifyPlugin CSS :MiniCssExtractPlugin HTML:HtmlWebpackPlugin
- gzip压缩:通过在Nginx等Web服务器上开启gzip压缩,进一步减少传输过程中的文件大小,提升加载速度。
nginx 开启 gzip加速
- 去除非必要代码:优化和精简代码,删除未使用的代码和资源,减少页面加载的冗余内容。
- CDN加速:将静态资源部署到CDN(内容分发网络),加速全球范围内用户的访问速度,减少服务器负载和响应时间。
这些优化策略能显著提升前端页面的加载速度、用户体验和整体性能。
欧克👌,文章到这里就先搞一段落了,文章中提到的具体详细的解决方案大家可以进行详细查阅有任何问题都可以在评论中留言,欢迎讨论
作者:洞窝-重阳
转载自:https://juejin.cn/post/7396542465299267647