Skip to content

react实现大图片分片加载

🕒 Published at:

在渲染一些大图片的时候往往页面会变得特别卡顿

通常我们可以采用压缩的方式去让图片缩小,但是往往也有一些场景下就是大图怎么办

分块懒加载

图像分块

在分块懒加载中,一张超大图像会被分割成多个小块(称为chunk)。每个小块包含图像的一部分,通常根据设定的分块大小来决定每个小块的宽高。分块后,整个大图像就变成了一个个小的矩形区域。

按需加载(懒加载)

与传统的图片加载方式不同,分块懒加载会根据用户当前的可视区域(视口)动态加载图片块。只有用户滚动到特定区域时,相关的小块才会被加载。这种方式避免了在页面加载时一次性加载所有的图片数据,从而节省了带宽并优化了性能。

实现步骤

我们可以思考一下,我们需要将一张大图分块懒加载,我们需要怎么做:

    • 获取图片原始宽高:在渲染前获取图片的实际尺寸。 设定分块大小:确定每个分块的尺寸,并计算出需要的横向和纵向分块数量。
  • 裁切图片:在渲染前裁切图片,使每个小块的尺寸符合设定的分块大小。
  • 分块坐标:遍历计算出的分块数量,生成每个分块的坐标信息。
  • 懒加载:使用 img 标签渲染每个分块,并通过 loading="lazy" 属性实现懒加载。

其中,渲染前获取图片信息和裁切是通过浏览器技术无法支持的,因为我都还没渲染出来,我怎么能拿到一个网络图片的信息呢?所以我们同样需要使用到阿里云的数据处理(x-oss-process)对图片进行在线处理。

x-oss-process=image/info

通过在图片URL中添加info参数的方式,会返回图片的基本信息,例如图片大小、格式、图片高度以及图片宽度等。

json
{
  "FileSize": {"value": "21839"},
  "Format": {"value": "jpg"},
  "FrameCount": {"value": "1"},
  "ImageHeight": {"value": "267"},
  "ImageWidth": {"value": "400"},
  "ResolutionUnit": {"value": "1"},
  "XResolution": {"value": "1/1"},
  "YResolution": {"value": "1/1"}
}

x-oss-process=image/crop

自定义裁剪功能可以根据自己的需要在原图的基础上裁切出需要的图片。

我们此次用到的参数有下面几个:

参数描述取值范围
w指定裁剪宽度。[0,图片宽度]默认为最大值。
h指定裁剪高度。[0,图片高度]默认为最大值。
x指定裁剪起点横坐标(默认左上角为原点)。[0,图片边界]
y指定裁剪起点纵坐标(默认左上角为原点)。[0,图片边界]

特别需要注意的是,在计算图片的边界情况时,需要计算边界的实际宽高,而不是直接使用固定的分块大小。这样可以避免出现渲染重复内容的问题。

js
import { useEffect, useState } from "react";
import "./App.css";
import axios from "axios";

function App() {
  const [imageInfo, setImageInfo] = useState({
    width: 0,
    height: 0,
  });
  const [chunks, setChunks] = useState<{ x: number; y: number }[]>([]);

  // 分块大小 可以根据需求调整
  const CHUNK_SIZE = 500;

  useEffect(() => {
    async function fetchImageInfo() {
      const res = (
        await axios.get(
          `xxxx.png?x-oss-process=image/info` //替换成某个阿里云托管图片的 url
        )
      )?.data;

      const info = {
        width: res.ImageWidth.value,
        height: res.ImageHeight.value,
      };
      setImageInfo(info);

      // 根据设定的分块大小(CHUNK_SIZE),计算需要的横向和纵向分块数量。
      // 使用 Math.ceil() 确保即使最后一块不足 CHUNK_SIZE 也会被覆盖。
      const horizontalChunks = Math.ceil(info.width / CHUNK_SIZE);
      const verticalChunks = Math.ceil(info.height / CHUNK_SIZE);

      // 创建一个空数组来存储分块信息
      const newChunks = [];
      // 遍历纵向分块
      for (let y = 0; y < verticalChunks; y++) {
        // 遍历横向分块
        for (let x = 0; x < horizontalChunks; x++) {
          // 将每个分块的坐标添加到数组中
          newChunks.push({ x, y });
        }
      }
      setChunks(newChunks);
    }
    fetchImageInfo();
  }, []);

  return (
    <div
      className="app"
      style={{
        position: "relative",
        // 设置容器的大小为图片的尺寸
        width: imageInfo?.width || "100%",
        height: imageInfo?.height || "100%",
        margin: "0 auto",
        overflow: "auto",
      }}
    >
      {chunks.map(({ x, y }) => {
        // 计算实际的宽度,取分块大小和剩余宽度的最小值
        const actualWidth = Math.min(
          CHUNK_SIZE,
          imageInfo.width - x * CHUNK_SIZE
        );
        // 计算实际的高度,取分块大小和剩余高度的最小值
        const actualHeight = Math.min(
          CHUNK_SIZE,
          imageInfo.height - y * CHUNK_SIZE
        );

        return (
          <img
            key={`${x}-${y}`}
            loading="lazy"
            src={`xxxx.png?x-oss-process=image/crop,x_${
              x * CHUNK_SIZE
            },y_${y * CHUNK_SIZE},w_${actualWidth},h_${actualHeight}`}
            style={{
              position: "absolute",
              left: x * CHUNK_SIZE,
              top: y * CHUNK_SIZE,
              width: actualWidth,
              height: actualHeight,
              // 设置分块的边框为红色,方便查看分块
              border: "1px solid red",
            }}
            alt={`chunk-${x}-${y}`}
            onError={(e) => console.error("加载失败:", e)}
            onLoad={() => console.log(`加载分块成功: ${x}-${y}`)}
          />
        );
      })}
    </div>
  );
}

export default App;

通过这种分块懒加载的方式,我们显著提高了渲染超大图片的性能。因为我们只渲染用户当前视口内可见的部分,其他部分则通过懒加载的方式动态加载。这样不仅能减少初始加载时间,还能显著降低对设备性能的需求,尤其是在移动设备上,能够有效避免因图片过大导致的卡顿或崩溃问题。