图片过大或过多显示优化方案

1,683 阅读3分钟

背景:

解决页面图片太多或者图片体积太大导致网页加载慢,用户体验不好问题

解决思路

  1. 采用渐进式加载:原图未加载完时显示比它内存小的模糊图;
  2. 采用懒加载:只加载可视区域的图片,即滚动到可视区域时再加载图片。

1. 解决图片过大显示问题

jpeg使用渐进式图片格式:

问题分析: 像一些大的背景图,轮播图或者banner图,我们一般使用jpeg,其中jpeg有两种格式:

baseline-jpeg(原始jpeg,一般项目的UI切图都是使用这种):存储方式是从上到下扫描一遍,逐行顺序保存; preogressive-jpeg(渐进式jpeg):和baseline-jpeg一遍扫描不同,preogressive-jpeg文件包含多次扫描,这些扫描从模糊到清晰顺序存储在JPEG文件中;

图片读取的顺序与存储顺序一致,如果文件较大或者网络下载速度较慢,使用baseline-jpeg就会看到图片被一行行加载的效果,而使用preogressive-jpeg,会先显示整个图片的模糊轮廓,随着扫描次数的增加,图片变得越来越清晰。因此更推荐使用preogressive-jpeg。

image.png

可行性分析: 使用imagemin-mozjpeg 库,它默认会将图像转换成渐进式jpeg, 且由于编码方式,它们比baseline-jpeg略小。同时,该库还能 配置压缩图片。在大多数情况下,quality 设置为 70,可以产生 足够清晰的图像。

封装通用图片代理组件

当图像格式不是jpeg时,也可以使用图片代理组件在图片未加载出来之前先显示缩略图

import React, { useState, useEffect, useRef, useCallback, ReactElement } from 'react';
import EmptyImage from '@/assets/images/png_empty_box.png';
interface IProxyImage {
  src: string; // 真实图片
  defaultImgSrc?: string; // 占位图
  [key: string]: any;
}

function loadImg(url: string) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.src = url;
    img.onload = () => {
      // resolve();
    };
    img.onerror = () => {
      reject();
    };
  });
}

export default function ProxyImage(props: IProxyImage) {
  const { src, defaultImgSrc = EmptyImage, ...restProps } = props;
  const [imgLoaded, setImgLoaded] = useState(false);
  const loadImg = useCallback((url: string) => {
    const img = new Image();
    img.src = url;
    img.onload = () => {
      setImgLoaded(true);
    };
  }, []);

  // 真实图片加载完成,显示真实图片
  useEffect(() => {
    loadImg(src);
  }, [src]);
  // 真实图片未加载完成,先显示占位图
  return <img src={imgLoaded ? src : defaultImgSrc} {...restProps} />;
}

使用方式:<ProxyImage src={item.productIcon} alt="" />

2. 解决图片过多显示问题

可行性分析: IntersectionObserver API 可以监听目标元素与其祖先或视窗交叉状态的手段,它能让你知道一个被观测的元素什么时候进入或离开浏览器的视口。我们可以使用IntersectionObserver API 封装通用的懒加载组件,监听目标元素滚动到可视范围时再触发加载图片

封装通用懒加载组件:

/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import React, { useState, useEffect } from 'react';

const options = {
  threshold: [0.5],
};
interface ILazyLoadComponent {
  onShowAction: (e: any) => void;
  children: any;
}

export default function LazyLoadComponent(props: ILazyLoadComponent) {
  const { onShowAction } = props;
  const [newChildren, setNewChildren] = useState<any>(null);
  useEffect(() => {
    // 利用React.Children.map遍历所有子组件
    const myNewChildren = React.Children.map(props.children, (child) => {
      // 判断child是不是一个React Element,防止渲染的时候出错
      if (child.type) {
        // 利用React.cloneElement复制子组件并为其新增props,lazy_container
        return React.cloneElement(child, {
          className: `${child.props.className} lazy_container`,
        });
      }
      return child;
    });
    setNewChildren(myNewChildren);
  }, []);

  useEffect(() => {
    if (!newChildren) {
      return;
    }
    // 拿到懒加载元素节点
    const containerSelector = document.querySelectorAll('.lazy_container');
    const animatedScrollObserver: IntersectionObserver = new IntersectionObserver((entries) => {
      entries.forEach((e): any => {
        // 节点进入视窗,执行回调(这里一般首页滚动加载动画只执行一次,因此执行回调后取消对元素的观察)
        if (e.isIntersecting) {
          onShowAction(e);
          animatedScrollObserver.unobserve(e.target);
        }
      });
    }, options);
    // 指定观察元素
    containerSelector.forEach((lazyContainer: any) => {
      animatedScrollObserver.observe(lazyContainer);
    });
  }, [newChildren]);
  return newChildren;
}

使用方式:

  const onShowAction = useCallback((e) => {
    e.target.classList.remove('inactive');
  }, []);
  
 <LazyLoadComponent onShowAction={onShowAction}>
 ...对直接子组件进行监听,当进入视窗时执行onShowAction回调
 </LazyLoadComponent>

扩展思考: 通用懒加载组件也可用户解决判断目标元素滚动到视窗时加载动画问题