漫谈前端性能优化

422 阅读14分钟

作为一名前端开发工程师,“性能”这个词,我们总是能从不同的人口中听到;性能也确实在方方面面影响着我们。对业务的影响、日常开发的影响,与我们密切相关。

为此,我查阅了不少资料,也学习了一些课程,最终,我决定写些什么!!来记录自己对前端性能优化的认知。

是什么?(What)

性能优化,非常牛掰的一个词汇。查了很多,基本意思都差不多:用最少的资源,提供最快、最稳定的服务

为什么?(Why)

这个就不用查官方了吧。用最少的钱办最多的事情,没有人喜欢迟缓的服务吧!

怎么做?(How)

借助最近看的一门课,梳理一下自己的收获,也来漫谈一下自己的性能优化。

0. 性能优化总纲

依托于用户从输入URL到浏览器加载完成经历了哪些阶段,来撑起整个性能优化过程:

  1. DNS域名解析:浏览器会先检查本地DNS缓存中是否有该域名对应的ID地址。如果没有,则向本地DNS服务器发起请求,进行域名解析
  2. TCP连接:浏览器与解析出的IP地址对应的服务器通过三次握手建立TCP连接
  3. 发出HTTP请求:浏览器向服务器发出HTTP请求
  4. 服务器处理请求并返回响应:服务器接收到请求后进行处理,并将数据放到HTTP的响应报文中返回给浏览器
  5. 浏览器渲染解析:浏览器拿到数据后,进行解析渲染。渲染完毕后,用户即可与页面进行交互

image.png 依托于这整个生命周期,前端能够做什么操作,来提高性能呢??我们一步步来看看吧!

1. 性能优化

DNS域名解析:

  1. DNS预解析:在页面头部添加<link>标签,通过设置rel="dns-prefetch"属性预解析页面中需要用到的域名。这样可以让浏览器在加载页面时就开始解析DNS,从而提升后续请求的速度。 image.png 我们可以看到掘金,就使用了DNS预解析的手段
  2. DNS缓存:个人理解需要有才能缓存,所以在缓存方面,思路还是尽可能少的进行请求;查阅了一些资料,都是建议使用CDN合并资源减少DNS查询次数等手段。

TCP连接:

这个阶段的性能优化,通过资料的查阅,发现前端能做的就是配合其他同学,使用协议(HTTP/2协议)等等其他一些更底层的操作。

DNS域名解析和TCP连接以上两个过程的优化往往前端和其他协助完成,前端可以做的不是很多。平时接触、听的也不是很多。查了很多资料,感觉好像确实自己能做的不多(自己仅仅只能代表自己了🐶),就姑且认为是这个样子吧!!!

平时讨论、看的文章基本都是在HTTP请求和浏览器渲染解析上做文章,自己所有的和性能优化的实践,也都是发生在该阶段的。

HTTP请求

该阶段,致力于尽量少的HTTP请求、单次HTTP请求最快。

减少单次HTTP请求时间

1. CDN

CDN(Content Delivery Network)即内容分发网络,指的是一组分布在世界各地的服务器。CDN通常用来存储静态内容,其通过缓存和就近原则提供最快的响应速度。 像我们日常开发时,会将图片、视频等静态资源上传到OSS或者七牛云,其都提供了CDN服务。 image.png

2. 图片优化

图片格式千千万,什么需求使用什么样的格式,都是有讲究的。

1. 格式选用
  • JPEG/JPG

这种格式的图片只能有损压缩,并且不支持透明。但其丰富的色彩,日常开发中经常作为大的背景图、轮播图这类。比如京东

image.png image.png
  • PNG

PNG格式的图片支持无损压缩;支持透明度。但是文件大小相对较大,并且不支持动画。 日常开发中,PNG格式一般需要保留细节和透明北京的图像,如logo、icon等。 同样是京东

image.png image.png

  • SVG

因为放大或者缩小不会失去清晰度和质量的特性,这使得SVG成为设计响应式和高分辨率也免得理想选择。前端开发中,SVG是制作图标的理想选择。

  • Base64

Base64 是一种用于传输 8Bit 字节码的编码方式,通过对图片进行 Base64 编码,我们可以直接将编码结果写入 HTML 或者写入 CSS,从而减少 HTTP 请求的次数。

  • WebP

WebP是一种现代的图像格式,可以提供更高的压缩率和更快的加载速度。既像PNG支持透明,还像GIF一样支持动画同时细节色彩丰富。早些年,因为兼容性问题,虽然完美,但使用较少;目前可以看can i use:除了古老的IE(已经摒弃了),2020年以后已经全部都支持了。 image.png

image.png

看了淘宝和京东的网页版,目前都有webp的使用

京东的应用

降级方案还是挺好玩的 image.png

2. 图片压缩

可以使用压缩工具,在确保清晰的情况下,将图片大小压缩。推荐工具[TinyPNG];(tinify.cn/)

也有配套的Vscode插件 image.png

3. 懒加载

简单来说就是不将页面内所有图片的静态资源加载,仅仅加载当前可是区域的图片资源,减少加载图片的数量来减少时间。当可视区域下移时,再加载对应的资源。 简单实现一个解决方案:

  1. 为需要延迟加载的图片添加一个自定义的属性,例如“data-src”,并将图片的真实路径作为该属性的值。
  2. 在页面加载完成后,使用JavaScript获取所有带有自定义属性的图片元素,并将它们的src属性设置为一个占位符图像。
  3. 监听窗口滚动事件,并判断每个带有自定义属性的图片是否在可视区域内。如果是,则将图片的src属性设置为data-src属性的值,实现图片的延迟加载。

以下是一个图片懒加载的示例代码:

    // 获取所有带有自定义属性的图片元素
const lazyImages = document.querySelectorAll('img[data-src]');

// 将图片的src属性设置为占位符图像
lazyImages.forEach(img => {
  img.setAttribute('src', 'placeholder.png');
});

// 监听窗口滚动事件
window.addEventListener('scroll', () => {
  // 判断每个带有自定义属性的图片是否在可视区域内
  lazyImages.forEach(img => {
    if (img.getBoundingClientRect().top < window.innerHeight) {
      // 将图片的src属性设置为data-src属性的值
      img.setAttribute('src', img.getAttribute('data-src')!);
      img.removeAttribute('data-src');
    }
  });
});
4. 图片优化总结

图片优化整体来说,就是质量和性能的考量;确保质量的前提下,用最小的体积图片;确保可视区域完成,加载请求最少的资源;

  • 我们根据不同图片格式的特性在不同场景下使用;
  • 也可以借助三方工具对现有的图片进行可以接受的压缩;
  • 也有懒加载方案,优先确保可视区域的显示,其他稍后加载,减少加载;
  • 在很早之前,也有一种名为雪碧图(精灵图)的解决方案,就是一张图,上有多种图片,我们通过定位的方式去获取对应的icon,减少图片请求加载次数;
3. 打包优化

打包优化我们目前最常见的两种打包方式老牌webpack和新型的vite;我了解到的,所有业务侧性能方面的优化,都是致力于:

  • 最终打包产物体积尽可能的小
  • 最终打包产物数量尽可能的少

绝对目标是这个,但在真正应用中,还是要具体情况具体对待,毕竟鱼和熊掌不可兼得,打包数量少和单个打包体积小肯定不能同时存在,这里需要我们执行人的取舍。

这也正正呼应了大标题:减少单次HTTP请求时间,资源小了,请求自然就快了。 以下是一些常见的 Webpack 打包优化方法:

  1. 使用 SplitChunksPlugin 插件,将公共的代码提取成单独的文件,以便多个页面可以共享这些代码。

  2. 使用 MiniCssExtractPlugin 插件,将 CSS 提取成单独的文件,以便浏览器可以并行加载多个 CSS 文件。

  3. 使用 Tree Shaking,将没有使用到的代码从打包结果中删除,以减少打包文件的大小。

  4. 使用 webpack-bundle-analyzer 插件,分析打包结果,找出打包文件中的冗余代码和不必要的依赖项,以便进一步优化打包结果。

手段很多,思想基本都是删除无用代码、提取公共代码、按需引入等等。

减少HTTP请求数量

单次HTTP请求时间是降下来了,那么如果请求次数过多,不还是性能会很差吗?对此,我们也有一些性能优化手段。

1. 防抖与节流

防抖节流,应用非常广泛,个人理解,都是防止某一个事件触发的过于频繁。

举个例子:搜索项,根据输入,进行搜索,频繁的输入框变更,频繁请求。 我们给输入框加上防抖节流方法,来控制值的变化,进而减少请求。

防抖和节流都是用于控制函数被频繁调用的情况,但它们的实现方式和应用场景略有不同。防抖是在一定时间内只执行一次函数,适用于处理连续的事件(如滚动、调整窗口大小等);而节流是每隔一定时间执行一次函数,适用于处理频繁的事件(如鼠标移动、输入等)。

简单实现一下:

每次触发防抖,都会将原来的计时器清零,重置计时器

// 防抖
function debounce(func, wait) {
  let timer = null;
  return function() {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, arguments);
    }, wait);
  }
}

节流在设置时间内,只有第一次事件可以进入到计时器中。

//节流
function throttle(func, wait) {
  let timer = null;
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, arguments);
        timer = null;
      }, wait);
    }
  }
}

有些情况下,我们可能需要将防抖和节流结合起来使用,以达到更好的效果。例如在处理页面滚动事件时,可以使用节流函数来限制滚动事件的执行频率,但是在滚动到底部时需要加载更多数据,此时可以使用防抖函数来避免多次加载数据。这样可以保证在滚动过程中不会频繁地加载数据,同时在滚动到底部时又能及时地加载数据。

以下是一个将防抖和节流结合起来使用的例子:

function throttleDebounce(func, wait, options) {
  let timerId;
  let lastArgs;
  let lastThis;
  let lastCallTime = 0;

  const leading = options.leading;
  const trailing = options.trailing;

  function invokeFunc(time) {
    const args = lastArgs;
    const context = lastThis;

    lastArgs = lastThis = null;
    lastCallTime = time;
    func.apply(context, args);
  }

  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime;

    if (lastArgs && timeSinceLastCall < wait) {
      if (timerId) {
        clearTimeout(timerId);
      }

      timerId = setTimeout(() => {
        invokeFunc(time);
      }, wait - timeSinceLastCall);

      return leading ? invokeFunc(time) : undefined;
    }

    if (lastArgs) {
      timerId = null;
      lastArgs = lastThis = null;
      return invokeFunc(time);
    }

    return undefined;
  }

  return function() {
    const context = this;
    const args = arguments;
    const time = Date.now();

    lastArgs = args;
    lastThis = context;

    if (!lastCallTime && !leading) {
      lastCallTime = time;
    }

    shouldInvoke(time);
  };
}

这个函数接受三个参数:要执行的函数、等待时间和选项参数。选项参数可以包含 leading 和 trailing,用于控制是否在等待时间的开始和结束时执行函数。如果 leading 为 true,则在等待时间开始时执行函数;如果 trailing 为 true,则在等待时间结束时执行函数。默认情况下,leading 和 trailing 都为 false

这个函数的实现方式比较复杂,但基本思路是先使用节流函数控制函数的执行频率,然后在满足一定条件时使用防抖函数来执行函数。具体来说,当函数被调用时,首先会记录函数的参数和上下文,并记录函数被调用的时间。然后会判断距离上一次函数调用的时间是否超过了等待时间,如果没有,则使用 setTimeout 来延迟函数的执行。如果距离上一次函数调用的时间已经超过了等待时间,则直接执行函数。在函数执行之前,会根据 leading 和 trailing 的值来判断是否需要在等待时间的开始和结束时执行函数。

2. 浏览器缓存

浏览器缓存是指浏览器将之前请求过的资源保存在本地,下次请求时直接从本地获取,而不是重新从服务器下载。这样可以加快页面加载速度,减轻服务器的负担。浏览器缓存主要有两种方式:强缓存和协商缓存。

强缓存

强缓存是指浏览器在一定时间内直接使用本地缓存,而不去请求服务器。强缓存可以通过设置响应头来实现。常见的设置方式有两种:Expires 和 Cache-Control。

  • Expires:过期时间,是一个绝对时间,表示缓存资源的有效期。当浏览器发起请求时,会将当前时间与过期时间进行比较,如果没有过期,则直接使用缓存。
  • Cache-Control:缓存控制,是一个相对时间,表示缓存资源的最大有效期。常见的取值有 max-age 和 no-cache。max-age 表示缓存资源的最大有效期,单位是秒;no-cache 表示不使用强缓存,需要进行协商缓存。

当浏览器进行强缓存时,会优先检查 Cache-Control,如果存在则使用 Cache-Control 中的设置,否则再检查 Expires。如果两者都不存在,则不进行强缓存。

协商缓存

协商缓存是指浏览器在使用缓存前,通过与服务器进行通信,以确定缓存是否可用。协商缓存可以通过设置响应头来实现。常见的设置方式有两种:Last-Modified 和 ETag。

  • Last-Modified:最后修改时间,表示资源最后一次修改的时间。当浏览器发起请求时,服务器会将该资源的最后修改时间返回给浏览器,浏览器将该时间保存在缓存中。下次请求时,浏览器会将该时间发送给服务器,服务器会将该时间与资源的最后修改时间进行比较,如果两者相同,则说明资源没有修改,可以使用缓存。
  • ETag:实体标识,是一个字符串,表示资源的唯一标识。当浏览器发起请求时,服务器会将该资源的 ETag 返回给浏览器,浏览器将该字符串保存在缓存中。下次请求时,浏览器会将该字符串发送给服务器,服务器会将该字符串与资源的 ETag 进行比较,如果两者相同,则说明资源没有修改,可以使用缓存。

当浏览器进行协商缓存时,会先检查 Cache-Control,如果存在且为 no-cache 或 no-store,则不使用缓存;否则再检查 Last-Modified 和 ETag。如果两者都不存在,则不进行协商缓存。如果存在 Last-Modified 或 ETag,则浏览器会将其发送给服务器,服务器会根据这些信息来判断是否可以使用缓存。如果资源没有修改,则服务器返回 304 Not Modified,浏览器直接使用缓存;否则返回新的资源,并返回新的 Last-Modified 或 ETag。

强缓存和协商缓存都可以有效地减少请求次数,提高页面加载速度。强缓存适用于资源不会经常变化的情况,协商缓存适用于资源可能经常变化的情况。在实际开发中在实际开发中,可以根据实际情况来选择使用强缓存和协商缓存。通常来说,对于静态资源(如图片、样式、脚本等),可以使用强缓存,因为这些资源不会频繁变化;对于动态资源(如 HTML 页面、接口数据等),可以使用协商缓存,因为这些资源可能会频繁变化。

浏览器解析渲染

浏览器层面我们可以做的性能优化也有很多。比如:回流、重绘需要避免;css样式方面的编码规则; 因为东西太多了,这里我用我做的一个思维导图来展示一下:

image.png

image.png

总结

性能优化可以说是一个浩瀚的海洋,我这里仅仅只展示了冰山一角。但我觉得只要按照这个链路来,有性能优化思想,遇到问题知道从哪里思索,从哪里入手,就是可以的。 HTTP请求和浏览器渲染,在这里前端可以说是大有作为。这里仅仅说的是对外的性能优化,还有前端自己的性能优化,比如打包时间、热更新时间,都是需要我们注意的,毕竟也不能苦了自己不是。

最后,因为个人见解,如果有什么不对的地方,欢迎指出!!! 感谢老板.gif