JS综合解决方案4——Web性能优化

220 阅读7分钟

性能优化是每个软件开发过程中绕不过去的一个话题。

作为Web项目,一般都是直面用户的,如果性能不好,可能就直接导致了用户体验不佳,无法长期使用,在市场化环境下没有竞争力

标准

想要衡量一个Web项目的性能,可以通过Chrome控制台中的NetworkPerformance两个部分

Network

Network标签页中有几个参数可以用来衡量性能

Chrome控制台Network.png

  • requests:已加载的资源数量,通常相同条件下,要加载的数量越少则性能越好
  • DOMContentLoadedHTML文档被完全解析所用的时间,不包括样式和内部script的加载
  • Load:页面完全加载的用时

Performance

Performance标签页可以在网页加载过程中记录各个环节所用时间,最后展示出来

Chrome控制台Performance.png

其中有几个参数的含义如下

  • FP(First Paint)首次绘制:浏览器在页面加载过程中首次在屏幕上绘制内容的时间,可以理解成第一次看到白屏的时间

    • FP 标记了页面从空白状态变为可见的第一个时间点,反映了页面加载的初步响应
  • FCP(First Contentful Paint)首次内容绘制:浏览器首次绘制出页面上任何内容(如文本图片SVG)的时间

    • FCP 是一个重要指标,因为它表明用户已经看到一些页面内容,可以确认页面正在加载。
  • LCP(Largest Contentful Paint)最大内容绘制:衡量页面上最大可见内容块(如大图像或大标题文字)完成绘制的时间

    • LCP 是一个关键指标,反映了页面的主要内容何时对用户可见,是用户对页面加载完成的感知基准
    • LCP 的理想时间通常在 2.5 秒以内,以确保用户有较好的体验。

优化方案

这里列举出的优化方案是工作中比较常用的,包括

  • 数据懒加载
  • 图片懒加载
  • webpack打包体积过大和CDN优化
  • 其他(gzipHTTP缓存等)

数据懒加载

数据懒加载在项目中很常见,例如下图

数据懒加载案例.png

上图中,每个模块(手机/智能穿戴)的数据对应一个请求,而还有一个内容超出了页面高度,一开始用户是看不到的

如果一开始就加载了所有的内容,那势必在浪费请求资源,而且用户也不一定会滚动到最后

所以,对此要做的就是:在用户快要看到内容的时候,再去做数据接口的请求

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()
    }
  }
)

图片懒加载

图片懒加载的逻辑其实和数据懒加载差不多,也是在用户还没有看到当前图片的时候,先不加载图片,否则请求的图片资源数量会很多

图片懒加载案例.png

处理思路和数据懒加载也很像

  • 监听图片是否可见
  • 如果可见,再加载图片

但是对于图片来说,它和数据不一样,一个页面中的图片相关的DOM元素太多了,如果逐个监听,势必非常麻烦也不好维护,所以需要封装一个通用的方法

Vue自定义指令

Vue3可以封装自定义指令,类似于v-if这种

针对图片懒加载我们可以尝试封装v-imgLazy指令,大致思路如下

  • 缓存应该展示的图片的路径(根据imgsrc属性
  • 替换占位图到图片上
  • 图片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构建项目的时候,可以看到一个关于部署包体积的输出表格

部署包体积的输出表格.png

如果引入了一些比较大的包,例如echartsxlsx等,打包时候一般会有这种提示,即某些入口文件大小超过了推荐的大小,可能会导致性能问题(加载慢等)

image.png

为了处理这个问题,要分两个步骤

  • 问题怎么产生的
  • 怎么处理

包大小分析的指令

vuecli提供了一个report参数,可以在打包的时候同时生成一个report的HTML文件,用来查看打包之后包的大小

npm run build --report

查看report分析结果可以知道,打包体积大也主要是因为引入的一些大体积依赖(例如echarts、xlsx)

包大小分析结果.png

所以处理方法也呼之欲出了,就是把这三个包在打包时候排除,用一些别的方法来引入(例如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压缩可以压缩htmlcssjs文件中的重复部分,重复率越高压缩越多

gzip的压缩是Nginx的配置返回,在服务端处理

HTTP缓存

这里的缓存指的是304状态码,即服务器告诉客户端文件没有变化,直接读取缓存资源即可

Service Worker

一个JS API,本意是为用户提供更好的离线体验,详细的在MDN上有描述