React 中的延迟加载图像
延迟加载是几乎所有资源密集型网站都采用的一种常见性能优化技术。我们经常会遇到这样的网页:先加载模糊版的图片,然后再加载高分辨率图片。虽然加载内容的总时间很长,但它对用户体验的影响却是显而易见的。
整个交互过程分为三个步骤:
-
等待内容进入视图后再开始加载图像。
-
一旦图像可见,就会加载具有模糊效果的轻量级缩略图,并发出原始图像的资源获取请求。
-
一旦原始图像完全加载,缩略图就会隐藏,并显示原始图像。
如果你曾经使用过 Gatsby,那么你肯定遇到过一个能帮GatsbyImage
你实现类似功能的组件。在本文中,我们将在 React 中实现一个类似的自定义组件,该组件使用IntersectionObserver
浏览器 API 逐步加载进入视图的图像。
尽管 Gatsby Image 的功能远不止模糊和加载图像,但我们只关注这一部分:
让我们来构建它。
构建整个事物的第一步是创建图像组件的布局。
这部分非常简单。出于本文的目的,我们将动态迭代一组图像并渲染一个ImageRenderer
组件。
import React from 'react';
import imageData from './imageData';
import ImageRenderer from './ImageRenderer';
import './style.css';
export default function App() {
return (
<div>
<h1>Lazy Load Images</h1>
<section>
{imageData.map(data => (
<ImageRenderer
key={data.id}
url={data.url}
thumb={data.thumbnail}
width={data.width}
height={data.height}
/>
))}
</section>
</div>
);
}
下一步是在ImageRenderer
组件内部渲染图像的占位符。
当我们以指定的宽度渲染图像时,它们会根据纵横比(即原始图像的宽度与高度的比率)调整其高度。
由于我们已经将原始图像的宽度和高度作为 props 传递给ImageRenderer
组件,因此我们可以轻松计算出宽高比,并以此计算图像占位符的高度。这样做是为了确保当图像最终加载完成时,占位符不会再次更新其高度。
占位符的高度是使用padding-bottom
CSS 属性以百分比形式设置的。
当以百分比指定时,填充的大小将按元素宽度的百分比计算。代码如下:
import React from 'react';
import './imageRenderer.scss';
const ImageRenderer = ({ width, height }) => {
return (
<div
className="image-container"
ref={imgRef}
style={{
paddingBottom: `${(height / width) * 100}%`,
width: '100%'
}}
/>
);
};
export default ImageRenderer;
.image-container {
background-color: #ccc;
overflow: hidden;
position: relative;
max-width: 800px;
margin: 20px auto;
}
到目前为止,我们的应用程序看起来是这样的:
使用交叉口观察器检测可见性
我们现在需要知道的是,图像容器何时进入视图。Intersection Observer 是完成这项任务的完美工具。
“Intersection Observer API 提供了一种异步观察目标元素与祖先元素或顶级文档视口的交集变化的方法。
交叉口观察器 API 允许您配置一个回调,当以下任一情况发生时调用该回调:
目标元素与设备的视口或指定元素相交。在 Intersection Observer API 中,该指定元素被称为根元素或根。
第一次要求观察者观察目标元素。”
我们将使用一个全局IntersectionObserver
实例来观察所有图像。我们还将保留一个监听器回调映射,该映射将由各个图像组件添加,并在图像进入视口时执行。
为了维护目标到监听器回调的映射,我们将使用WeakMap
来自 Javascript 的 API。
“
WeakMap
对象是键/值对的集合,其中键是弱引用。键必须是对象,值可以是任意值。”
我们编写了一个自定义钩子来获取IntersectionObserver
实例,将目标元素作为观察者添加,并向地图添加一个监听器回调。
import { useEffect } from 'react';
let listenerCallbacks = new WeakMap();
let observer;
function handleIntersections(entries) {
entries.forEach(entry => {
if (listenerCallbacks.has(entry.target)) {
let cb = listenerCallbacks.get(entry.target);
if (entry.isIntersecting || entry.intersectionRatio > 0) {
observer.unobserve(entry.target);
listenerCallbacks.delete(entry.target);
cb();
}
}
});
}
function getIntersectionObserver() {
if (observer === undefined) {
observer = new IntersectionObserver(handleIntersections, {
rootMargin: '100px',
threshold: '0.15',
});
}
return observer;
}
export function useIntersection(elem, callback) {
useEffect(() => {
let target = elem.current;
let observer = getIntersectionObserver();
listenerCallbacks.set(target, callback);
observer.observe(target);
return () => {
listenerCallbacks.delete(target);
observer.unobserve(target);
};
}, []);
}
如果我们没有为 IntersectionObserver 指定任何根元素,则默认目标被视为文档视口。
我们的IntersectionObserver
回调函数从地图中获取监听器回调,并在目标元素与视口相交时执行该回调。由于我们只需要加载一次图像,因此它会移除观察者。
使用 Intersectionobserver 作为 ImageRenderer 组件
在组件内部ImageRenderer
,我们使用自定义钩子useIntersection
,并传递图像容器的引用和一个回调函数,该函数将设置图像的可见性状态。代码如下:
import React, { useState, useRef } from 'react';
import classnames from 'classnames';
import { useIntersection } from './intersectionObserver';
import './imageRenderer.scss';
const ImageRenderer = ({ url, thumb, width, height }) => {
const [isInView, setIsInView] = useState(false);
const imgRef = useRef();
useIntersection(imgRef, () => {
setIsInView(true);
});
return (
<div
className="image-container"
ref={imgRef}
style={{
paddingBottom: `${(height / width) * 100}%`,
width: '100%'
}}
>
{isInView && (
<img
className='image'
src={url}
/>
)}
</div>
);
};
export default ImageRenderer;
.image-container {
background-color: #ccc;
overflow: hidden;
position: relative;
max-width: 800px;
margin: 20px auto;
.image {
position: absolute;
width: 100%;
height: 100%;
opacity: 1;
}
}
完成此操作后,我们的应用程序将如下例所示:
当我们滚动页面时,网络请求如下所示:
如您所见,我们的IntersectionObserver
作品和图片仅在进入视图时才会加载。此外,我们还看到,由于整张图片的加载,存在轻微的延迟。
现在我们有了延迟加载功能,我们将继续进行最后一部分。
添加模糊效果
添加模糊效果的方法是,除了实际图像外,还尝试加载低质量的缩略图,并filter: blur(10px)
为其添加属性。当高质量图像完全加载后,我们隐藏缩略图并显示实际图像。代码如下:
import React, { useState, useRef } from 'react';
import classnames from 'classnames';
import { useIntersection } from './intersectionObserver';
import './imageRenderer.scss';
const ImageRenderer = ({ url, thumb, width, height }) => {
const [isLoaded, setIsLoaded] = useState(false);
const [isInView, setIsInView] = useState(false);
const imgRef = useRef();
useIntersection(imgRef, () => {
setIsInView(true);
});
const handleOnLoad = () => {
setIsLoaded(true);
};
return (
<div
className="image-container"
ref={imgRef}
style={{
paddingBottom: `${(height / width) * 100}%`,
width: '100%'
}}
>
{isInView && (
<>
<img
className={classnames('image', 'thumb', {
['isLoaded']: !!isLoaded
})}
src={thumb}
/>
<img
className={classnames('image', {
['isLoaded']: !!isLoaded
})}
src={url}
onLoad={handleOnLoad}
/>
</>
)}
</div>
);
};
export default ImageRenderer;
.image-container {
background-color: #ccc;
overflow: hidden;
position: relative;
max-width: 800px;
margin: 20px auto;
}
.image {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
&.thumb {
opacity: 1;
filter: blur(10px);
transition: opacity 1s ease-in-out;
position: absolute;
&.isLoaded {
opacity: 0;
}
}
&.isLoaded {
transition: opacity 1s ease-in-out;
opacity: 1;
}
}
img
HTML 中的元素有一个属性onLoad
,该属性接受一个回调函数,该回调函数在图像加载完成后触发。我们利用此属性设置isLoaded
组件的状态,并在使用opacity
CSS 属性显示实际图像的同时隐藏缩略图。
您可以在此处找到本文的 StackBlitz 演示:
结论
所以我们有它:我们的自定义ImageRenderer
组件在图像进入视图时加载图像并显示模糊效果以提供更好的用户体验。
希望你喜欢这篇文章。你可以在我的GitHub 仓库中找到完整的代码。
感谢您的阅读!
如果您喜欢这篇文章,请考虑与您的朋友和同事分享
此外,如果您对本文有任何建议或疑问,请随时在 Twitter 上发表评论或直接给我发私信。
文章来源:https://dev.to/shubhamreacts/progressively-loading-images-in-react-40lg