性能优化(4)——资源优化

127 阅读3分钟

资源的压缩与合并

为什么我们要做资源的压缩与合并:

  • 减少请求资源的大小
  • 减少http请求的数量

HTML压缩

  • 使用在线工具进行压缩
  • 使用html-minifier等npm工具

CSS压缩

  • 使用在线工具压缩
  • 使用clean-css等npm工具

JS压缩与混淆

  • 使用在线工具压缩(不适合现代压缩方案)
  • 使用Webpack对JS在构建时进行压缩

CSS JS 文件合并

合并的情况是比较小的,因为我们现在主流的方案是渐进式。

  • 若干个小文件进行合并(减少http多次请求)
  • 无冲突,服务相同的模块(避免命名冲突)

图片优化

图片格式优化

  • JPEG/JPG

优点:有损压缩,较高的压缩比,色彩保存完整。

缺点:1. 边缘纹理较差。2. 行扫描的方式进行渲染,所以我们经常会发现大图片或者网络慢的情况下,是从上往下逐步渲染的。

使用场景:较大的图片展示轮播图之类的。

  • PNG

优点:边缘纹理较好。

缺点:体积较大。

使用场景:适用于比较小的图片。支持透明图片。

  • WebP

体积比PNG小,其他比较类似PNG。谷歌推出的,所有兼容性一般。和PNG差距不会特别明显。

  • 后面会具体讲到SVG

图片优化方案

1. 图片懒加载

  • 原生懒加载
  • 第三方懒加载

因为我是vue技术栈的,所以在懒加载方面,着重介绍下element-ui的源码,代码版本是2.15.7

在初始化的时候,当用户传入了lazy属性为true,开启懒加载。

mounted() {
  if (this.lazy) { // 如果开启懒加载
    this.addLazyLoadListener();
  } else {
    this.loadImage();
  }
},

先对父元素的scroll事件做了监听,并对滚动条事件做了节流处理。

// 这个方法主要是做 懒加载 的兼容和节流等处理
  addLazyLoadListener() {
    if (this.$isServer) return;

    const { scrollContainer } = this; // 开启懒加载后,监听 scroll 事件的容器
    let _scrollContainer = null;
    // 做各种判断,最终把 scroll 事件的容器 赋值给 _scrollContainer
    if (isHtmlElement(scrollContainer)) {
      _scrollContainer = scrollContainer;
    } else if (isString(scrollContainer)) {
      _scrollContainer = document.querySelector(scrollContainer);
    } else {
      _scrollContainer = getScrollContainer(this.$el);
    }

    if (_scrollContainer) {
      this._scrollContainer = _scrollContainer;
      // 滚动条事件节流
      this._lazyLoadHandler = throttle(200, this.handleLazyLoad);
      // on方法是一个兼容的dom监听事件,兼容addEventListener事件
      on(_scrollContainer, 'scroll', this._lazyLoadHandler);
      this.handleLazyLoad();
    }
  },

接下来我们看下滚动事件的回调函数handleLazyLoad做了什么?

  // 懒加载核心函数
  handleLazyLoad() {
    // isInContainer 判断dom元素是否在指定容器的视口中
    if (isInContainer(this.$el, this._scrollContainer)) {
      this.show = true;
      this.removeLazyLoadListener();
    }
  },

上面我们看到只走了一个isInContainer函数判断,所以这个isInContainer函数就是重点了

// 判断dom元素是否在指定容器的视口中
export const isInContainer = (el, container) => {
  if (isServer || !el || !container) return false;
  // getBoundingClientRect api 其提供了元素的大小及其相对于视口的位置。
  // 除了 width 和 height 以外的属性是相对于视图窗口的左上角来计算的。
  const elRect = el.getBoundingClientRect();
  let containerRect;
  // 如果视口是浏览器窗口,就不需要getBoundingClientRect计算
  if ([window, document, document.documentElement, null, undefined].includes(container)) {
    containerRect = {
      top: 0,
      right: window.innerWidth,
      bottom: window.innerHeight,
      left: 0
    };
  } else {
    containerRect = container.getBoundingClientRect();
  }

  return elRect.top < containerRect.bottom &&
    elRect.bottom > containerRect.top &&
    elRect.right > containerRect.left &&
    elRect.left < containerRect.right;
};

我们可以从上面那个函数看到,element-ui是通过官方getBoundingClientRect的api,获取到了dom的位置信息。

getBoundingClientRect的位置信息是:除了 width 和 height 以外的属性是相对于视图窗口的左上角来计算的。通过获取到的位置信息,就可以判断出我们传入的el是否在container的区域内。

最后一步我们new Image(),当图片加载完成后,会执行回调函数onload事件,最后修改loading和error为false,最终把图片展示出来

// 加载图片
loadImage() {
    if (this.$isServer) return;

    // reset status
    this.loading = true;
    this.error = false;

    const img = new Image();
    // 图片加载完成
    img.onload = e => this.handleLoad(e, img);
    img.onerror = this.handleError.bind(this);

    // bind html attrs 
    // so it can behave consistently
    Object.keys(this.$attrs)
      .forEach((key) => {
        const value = this.$attrs[key];
        img.setAttribute(key, value);
      });
    img.src = this.src;
},
// 图片加载完成回调,展示图片
handleLoad(e, img) {
    this.imageWidth = img.width;
    this.imageHeight = img.height;
    this.loading = false;
    this.error = false;
},

懒加载方案拓展方案IntersectionObserver

我们可以发现element-ui采用的是getBoundingClientRect api实现的,除了这种方案外,还有一种较为方便的方案IntersectionObserver

上面那种方法虽然能够实现图片懒加载,但需要自己手动去计算,并且会引起回流与重绘,性能相对来说较差。

示例:

function lazyLoadWithObserver() {
    // 推荐使用IntersectionObserver
    let observer = new IntersectionObserver((entries, observe) => {
        entries.forEach(item => {
            // 获取当前正在观察的元素
            let target = item.target
            if(item.isIntersecting && target.dataset.src) {
                target.src = target.dataset.src
                // 删除data-src属性
                target.removeAttribute('data-src')
                // 取消观察
                observe.unobserve(item.target)
            }
        })
    })

    let imgs = document.querySelectorAll('.img_box')

    imgs.forEach(item => {
      // 遍历观察元素
        observer.observe(item)
    })
}

lazyLoadWithObserver()

2. 渐进式图片

主要是利用JPEG的一种保存方式。

JPEG的保存方式是有两种的:

  • Baseline JPEG(基准式) 从上至下渲染
  • Progressive JPEG(渐进式) 从模糊到清晰

虽然等待的总事件更长,但是体验过程中比较舒适。

渐进式解决方案

  • progressive-image
  • ImageMagick
  • libjpeg
  • jpegtran
  • jpeg-recompress
  • imagemin

3. 使用响应式图片

  • Srcset属性的使用(同样的图片不同尺寸)
  • Size属性的使用
  • picture的使用

字体优化

什么是FOIT和FOUT?

  • 字体未下载完成时,浏览器隐藏或自动降级,导致字体闪烁
  • Flash Of Invisible Text
  • Flash Of Unstyled Text

在字体加载后,会覆盖之前的字体,中间的过程中会发生字体闪烁。

解决方案 font-display

font-display的属性

  • auto 默认值
  • block
  • swap
  • fallback 最常使用
  • optional 最常使用

参考资料:

从图片懒加载来看IntersectionObserver