原文地址: 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;
由于,我们没有定义 root
。IntersectionObserver
默认为可视窗口。我们定义了 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.js
和 components/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 只有在图片进入可视区域才会渲染图片。首先显示一张模糊的缩略图,然后渐进式的显示完整的图片。
完整的代码可以在这查看。