React 中渐进式加载图片

6,086 阅读4分钟

原文地址: medium.com/frontend-di…
译文地址:github.com/xiao-T/note…
本文版权归原作者所有,翻译仅用于学习。

你是否好奇 Medium 是如何加载图片的?或许,你已经注意到图片是分多个步骤加载渲染的。首先,显示一张模糊版本的图片,然后,用全尺寸的图片替换掉。

Medium 如何加载图片

  • 直到图片进入可是范围才加载图片
  • 然后,加载一张模糊的缩略图
  • 然后,加载全尺寸图片,并替换掉缩略图

我们可以把图片加载技术分为两个不同的功能。

1. 懒加载

懒加载是在页面加载时延迟加载非关键资源的一种技术。那些,非关键的资源按需加载。对于图片,非关键通常代表着屏幕外 — developers.google.com

懒加载是一种非常好的技术,它可以明显提升网站的性能。

想象一下,你写了一篇 10 分钟长的博客,里面有 20 张高分辨率图片。如果,这些图片一次性加载,博客将会变得很慢。通过,懒加载我们可以按需加载。无论内容有多少,我们只渲染那些用户看到即可。

2. 模糊处理

为了更快的显示内容,我们需要显示一张模糊了的小图并放大到全尺寸。当全尺寸的图片加载完,然后替换掉它。

如果,你之前用过 Gatsby,你应该用过 gatsby-image。它为我们提供上面提到的技术,并不需要自己实现。

但是,作为一个开发者,我喜欢自己实现。

我们来实现它

首先,我们分析下问题

  • 我们需要知道哪些图片进入了可视范围
  • 一旦图片进入可视范围,我们需要加载缩略图和全尺寸图片
  • 一旦全尺寸的图片加载完成,我们需要替换掉缩略图
  • 在加载图片时,我们要保证页面不能抖动。我们占位的容器大小应该和最终图片的大小保持一致

我们开始

我们先用 create-react-app 创建一个 React 应用的骨架。

npx create-react-app progressive-images

我们将会使用 Unsplash 中的图片。我使用 Unsplash API 获取了最新的 10 张图片。并它们保存在 Github Gist 中。

把 gist 的内容复制到名为 images.json 的文件中。

打开 App.js 并用以下的内容替换掉它。

import React from "react";
import images from "./images.json";
import ImageContainer from "./components/image-container";
import "./App.css";
function App() {
  return (
    <div className="app">
      <div className="container">
        {images.map(res => {
          return (
            <div key={res.id} className="wrapper">
              <ImageContainer
                src={res.urls.regular}
                thumb={res.urls.thumb}
                height={res.height}
                width={res.width}
                alt={res.alt_description}
              />
            </div>
          );
        })}
      </div>
    </div>
  );
}
export default App;

然后,打开 App.css 并用以下内容替换

.app {
  display: flex;
  justify-content: center;
  padding-top: 1em;
}
.container {
  width: 100%;
  max-width: 600px;
}
.wrapper {
  padding: 1em 0;
}

现在,我们来创建 components/image-container.js

在处理图片渲染之前,我们先创建一个容器。

import React from "react";
import "./image-container.css";const ImageContainer = props => {
  const aspectRatio = (props.height / props.width) * 100;return (
    <div
      className="image-container"
      style={{ paddingBottom: `${aspectRatio}%` }}
    />
  );
};

然后是 image-container.css

.image-container {
  position: relative;
  overflow: hidden;
  background: rgba(0, 0, 0, 0.05);
}

首先,我们需要计算出图片的宽高比例。通过 width/height 可以获得。然后,用这个值设置容器的 padding-bottom

比如,一张 1024 x 768 图片的宽高比就是 0.75。我就会把容器的 padding-bottom 设置为 75%

我们可以通过 yarn start 运行 app,并会看到以下结果。

现在,我们已经有了一些 box,它们的尺寸和需要渲染的图片一致。

Intersection Observer

现在,我们需要一种方法监控图片是否进入了可视范围。我们可以使用新的浏览器 API IntersectionObserver 来实现。

Intersection Observer API提供了一种异步观察目标元素与祖先元素或顶级文档viewport的交集中的变化的方法 — developer.mozilla.org

我们来实现一个自定义 Hook:hooks/use-intersection-observer.js

import React from "react";
const useIntersectionObserver = ({
  target,
  onIntersect,
  threshold = 0.1,
  rootMargin = "0px"
}) => {
  React.useEffect(() => {
    const observer = new IntersectionObserver(onIntersect, {
      rootMargin,
      threshold
    });
const current = target.current;
observer.observe(current);
return () => {
      observer.unobserve(current);
    };
  });
};
export default useIntersectionObserver;

由于,我们没有定义 rootIntersectionObserver 默认为可视窗口。我们定义了 threshold: 0.1。这代表着当目标元素的 10% 进入到可视窗口,我们的回调就会执行。

使用自定义 Hook

使用自定义 Hook 时,我们需要指定一个 target 和相应的回调函数。

目标元素将会用 React ref 引用着容器 div。

我们的回调方法将会设置一个 state 变量,用来标示图片是否可见。然后执行 observer.unobserve。一旦图片可见,我们就不需要 IntersectionObserver 去监控它了。

image-container.js 中按照以下内容调整。

import React from "react";
import useIntersectionObserver from "../hooks/use-intersection-observer";
import "./image-container.css";
const ImageContainer = props => {
  const ref = React.useRef();
  const [isVisible, setIsVisible] = React.useState(false);
useIntersectionObserver({
    target: ref,
    onIntersect: ([{ isIntersecting }], observerElement) => {
      if (isIntersecting) {
        setIsVisible(true);
        observerElement.unobserve(ref.current);
      }
    }
  });
const aspectRatio = (props.height / props.width) * 100;
return (
    <div
      ref={ref}
      className="image-container"
      style={{ paddingBottom: `${aspectRatio}%` }}
    >
      {isVisible && (
        <img className="image" src={props.src} alt={props.alt} />
       )}
    </div>
  );
};
export default ImageContainer;

现在,当组件进入可视范围时就会渲染全尺寸的图片。

我们来看看具体效果

很好!我们的应用已经可以懒加载图片了。图片只有在进入可视区域才会加载。

如果,你打开开发者工具中的 network 选项,你们会看到它实际的效果。看一下 Waterfall

添加模糊技术

首先创建两个文件: components/image.jscomponents/image.css

我们会在组件 Image 中渲染两张图片:全尺寸图片和缩略图。但全尺寸图片加载完成就隐藏缩略图。

复制以下代码到 components/image.js 中。

import React from "react";
import "./image.css";
const Image = props => {
  const [isLoaded, setIsLoaded] = React.useState(false);
  return (
    <React.Fragment>
      <img
        className="image thumb"
        alt={props.alt}
        src={props.thumb}
        style={{ visibility: isLoaded ? "hidden" : "visible" }}
      />
      <img
        onLoad={() => {
          setIsLoaded(true);
        }}
        className="image full"
        style={{ opacity: isLoaded ? 1 : 0 }}
        alt={props.alt}
        src={props.src}
      />
    </React.Fragment>
  );
};
export default Image;

以下是 components/image.css 的内容。

.image {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.full {
  transition: opacity 400ms ease 0ms;
}
.thumb {
  filter: blur(20px);
  transform: scale(1.1);
  transition: visibility 0ms ease 400ms;
}

现在,我来最后一次运行下我们的应用。

确保打开了 devtools 并禁用了缓存。

总结

我们制作了一个带有图片懒加载技术的 React App。我的 App 只有在图片进入可视区域才会渲染图片。首先显示一张模糊的缩略图,然后渐进式的显示完整的图片。

完整的代码可以在查看。