性能优化是每个软件开发过程中绕不过去的一个话题。
作为Web项目,一般都是直面用户的,如果性能不好,可能就直接导致了用户体验不佳,无法长期使用,在市场化环境下没有竞争力
标准
想要衡量一个Web项目的性能,可以通过Chrome控制台中的Network和Performance两个部分
Network
Network标签页中有几个参数可以用来衡量性能
requests:已加载的资源数量,通常相同条件下,要加载的数量越少则性能越好DOMContentLoaded:HTML文档被完全解析所用的时间,不包括样式和内部script的加载Load:页面完全加载的用时
Performance
Performance标签页可以在网页加载过程中记录各个环节所用时间,最后展示出来
其中有几个参数的含义如下
-
FP(First Paint)首次绘制:浏览器在页面加载过程中首次在屏幕上绘制内容的时间,可以理解成第一次看到白屏的时间
- FP 标记了页面从空白状态变为可见的第一个时间点,反映了页面加载的初步响应
-
FCP(First Contentful Paint)首次内容绘制:浏览器首次绘制出页面上任何内容(如文本、图片或 SVG)的时间
- FCP 是一个重要指标,因为它表明用户已经看到一些页面内容,可以确认页面正在加载。
-
LCP(Largest Contentful Paint)最大内容绘制:衡量页面上最大可见内容块(如大图像或大标题文字)完成绘制的时间
- LCP 是一个关键指标,反映了页面的主要内容何时对用户可见,是用户对页面加载完成的感知基准
- LCP 的理想时间通常在 2.5 秒以内,以确保用户有较好的体验。
优化方案
这里列举出的优化方案是工作中比较常用的,包括
- 数据懒加载
- 图片懒加载
webpack打包体积过大和CDN优化- 其他(
gzip、HTTP缓存等)
数据懒加载
数据懒加载在项目中很常见,例如下图
上图中,每个模块(手机/智能穿戴)的数据对应一个请求,而还有一个内容超出了页面高度,一开始用户是看不到的
如果一开始就加载了所有的内容,那势必在浪费请求资源,而且用户也不一定会滚动到最后
所以,对此要做的就是:在用户快要看到内容的时候,再去做数据接口的请求
IntersectionObserver
Web API提供了一个接口IntersectionObserver,它可以用来检测目标元素和视窗区域的交叉大小,说白了就是用来检测目标元素是否可见
IntersectionObserver对象上有一个observe方法,接收的参数就是用来监听是否可见的DOM元素
const box3Target = ref(null)
onMounted(() => {
const intersectionObserver = new IntersectionObserver((entries) => {
if (entries[0].intersectionRatio <= 0) {
console.log('box3 不可见')
return
}
console.log('box3 可见')
})
intersectionObserver.observe(box3Target.value)
})
vue-use封装的useIntersectionObserver
在Vue3项目中,使用原生的IntersectionObserver有点麻烦,我们可以使用vue-use这个库里面封装好的useIntersectionObserver方法来实现元素是否可见的监听,从而实现数据懒加载
useIntersectionObserver方法接收两个参数
- 监听是否可见的
DOM对象 - 回调函数,其中第一个参数可以解构出监听的DOM对象是否可见的
boolean值
另外,useIntersectionObserver本身还返回一个stop方法,可以用来结束监听
const box3Target = ref(null)
const { stop } = useIntersectionObserver(
box3Target,
([{ isIntersecting }], observerElement) => {
// 一旦出现在视窗内,调用接口请求数据,同时结束监听(因为持续监听没有意义了)
if (isIntersecting) {
loadData03()
stop()
}
}
)
图片懒加载
图片懒加载的逻辑其实和数据懒加载差不多,也是在用户还没有看到当前图片的时候,先不加载图片,否则请求的图片资源数量会很多
处理思路和数据懒加载也很像
- 监听图片是否可见
- 如果可见,再加载图片
但是对于图片来说,它和数据不一样,一个页面中的图片相关的DOM元素太多了,如果逐个监听,势必非常麻烦也不好维护,所以需要封装一个通用的方法
Vue自定义指令
Vue3可以封装自定义指令,类似于v-if这种
针对图片懒加载我们可以尝试封装v-imgLazy指令,大致思路如下
- 缓存应该展示的图片的路径(根据
img的src属性) - 替换占位图到图片上
- 对
图片DOM进行监听 - 如果将要看到图片,把占位图替换成应该展示的图片,从而发出图片资源的请求
- 结束监听
自定义指令需要添加一个生命周期钩子,这里因为图片涉及了DOM渲染,所以必须在mounted周期中,钩子中有一个参数可以拿到当前指令绑定的DOM元素
import { useIntersectionObserver } from '@vueuse/core'
const imgLazy = {
mounted(el) {
// 图片懒加载:一开始不加载,等到将要看到的时候再加载
// 1.缓存当前图片的路径
const cacheSrc = el.src
// 2.把img.src变成占位图
el.src = 'https://res.lgdsunday.club/img-load.png'
// 3.监听将要看到
const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
if (isIntersecting) {
// 4.将要看到的时候,替换占位图为原有的图片
el.src = cacheSrc
// 5.结束监听
stop()
}
})
}
}
Vue插件
为了把这个指令绑定到全局,而非每次调用的时候都要用directive,需要使用Vue3中插件的概念,把自定义指令通过Vue.use的方式添加到全局
作为插件,需要是一个有install方法的对象
export default {
// app.use的方式使用
install: (app) => {
app.directive('imgLazy', imgLazy)
}
}
// 这样在main.js中就可以用Vue.use的方法绑定自定义指令了
createApp(App).use(store).use(router).use(directive).mount('#app')
项目打包体积处理
通常我们使用npm run build构建项目的时候,可以看到一个关于部署包体积的输出表格
如果引入了一些比较大的包,例如echarts、xlsx等,打包时候一般会有这种提示,即某些入口文件大小超过了推荐的大小,可能会导致性能问题(加载慢等)
为了处理这个问题,要分两个步骤
- 问题怎么产生的
- 怎么处理
包大小分析的指令
vuecli提供了一个report参数,可以在打包的时候同时生成一个report的HTML文件,用来查看打包之后包的大小
npm run build --report
查看report分析结果可以知道,打包体积大也主要是因为引入的一些大体积依赖(例如echarts、xlsx)
所以处理方法也呼之欲出了,就是把这三个包在打包时候排除,用一些别的方法来引入(例如CDN)
webpack排除指定的依赖
webpack可以添加一个externals属性,目的是打包时候不把某些依赖打包到静态js等文件中,而是在运行时再调用
externals需要以对象的形式填写,key是依赖名字,value是该依赖在全局中的名字
// 排除打包,只是在build时候排除,dev不需要
let externals = {}
// 用来判断是否是生产环境
const isProd = process.env.NODE_ENV === 'production'
if (isProd) {
externals = {
xlsx: 'XLSX',
echarts: 'echarts'
}
}
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
externals
}
})
使用CDN在运行时引入包
在webpack排除了几个较大的包之后,还需要在运行时导入这几个包,一般就是在script标签中引入CDN
这里需要使用htmlWebpackPlugin给打包后的HTML文件添加上被排除的依赖的CDN
// 排除打包,只是在build时候排除,dev不需要
let cdn = {
js: []
}
const isProd = process.env.NODE_ENV === 'production'
if (isProd) {
......
cdn = {
js: [
'https://cdn.jsdelivr.net/npm/echarts@5.4.2/dist/echarts.min.js',
'https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js'
]
}
}
module.exports = defineConfig({
......
chainWebpack(config) {
config.plugin('html').tap((args) => {
// 携带指定的属性到htmlWebpackPlugin
args[0].cdn = cdn
return args
})
}
})
配置完了还需要在public/index.html中做一个导入,其实就是最后生成script标签实现CDN引入
<!DOCTYPE html>
<html lang="">
<head>
......
</head>
<body>
......
<div id="app"></div>
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>"></script>
<% } %>
</body>
</html>
其他优化方案
gzip压缩
gzip压缩可以压缩html、css和js文件中的重复部分,重复率越高压缩越多
gzip的压缩是Nginx的配置返回,在服务端处理
HTTP缓存
这里的缓存指的是304状态码,即服务器告诉客户端文件没有变化,直接读取缓存资源即可
Service Worker
一个JS API,本意是为用户提供更好的离线体验,详细的在MDN上有描述