code
Вы когда-нибудь задумывались, как Medium загружает изображения? Возможно, вы заметили, как изображения отображаются в несколько этапов. В самом начале на экране появляется размытая версия изображения, которая затем заменяется полноразмерной версией.
Мы можем разделить эту технику загрузки изображений на две отдельные функции.
Ленивая загрузка - это метод, который откладывает загрузку некритических ресурсов во время загрузки страницы. Вместо этого эти некритические ресурсы загружаются в момент необходимости. Что касается изображений, «некритичный» часто является синонимом «вне экрана».— developers.google.com
Чтобы что-то отобразить на экране быстрее, мы показываем размытое крошечное изображение, масштабированное до полной ширины. Когда мы закончим загрузку полноразмерного изображения, мы меняем их местами.
Если вы раньше работали с Gatsby, вы, вероятно, использовали gatsby-image. Gatsby-image дает нам эти две техники без необходимости создавать их самостоятельно. Но мы разработчики. Мы любим все делать сами.
Прежде всего, проанализируем проблему.
Начнем с создания нового приложения React с помощью приложения create-react-app
.
npx create-react-app progressive-images
Будем использовать Unsplash , как базу данных изображений. Для этого получим с помощью API Unsplash массив из десяти первых попавшихся изображений. Этот массив сохранен в Github 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
, с компонентом ImageContainer
, отвечающим за контейнер для тех изображений, которые мы планируем загружать. Там же создадим файл image-container.css
со стилями для нашего компонента.
В компоненте ImageContainer
определяем соотношение сторон изображения в константеaspectRatio
. Оно рассчитывается в процентах путем деления ширины изображения на его на высоту и умножения на 100
. Затем мы добавляем свойство padding-bottom
к стилю нашего контейнера изображения с этим значением. Свойство padding-bottom
задает внутренний нижний отступ от содержания до нижней границы элемента. Например, изображение 1024 x 768 пикселей имеет соотношение сторон 0,75. В этом случае padding-bottom: 75%
.
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}%` }}
/>
);
};
export default ImageContainer;
И image-container.css
.
.image-container {
position: relative;
overflow: hidden;
background: rgba(0, 0, 0, 0.05);
}
Мы можем запустить наше приложение с yarn start
(или npm start
) и посмотреть, как оно выглядит.
Теперь у нас есть несколько блоков того же размера, что и изображения, которые мы хотим визуализировать.
API-интерфейс Intersection Observer обеспечивает способ асинхронного наблюдения за изменениями пересечения целевого элемента с элементом-предком или с окном просмотра документа верхнего уровня - developer.mozilla.org
Сделаем кастомный хук . Создаем директорию 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% таргета видны в области просмотра, вызывается callback функция.
Чтобы использовать этот кастомный хук, нам нужно вызвать его с таргетом и callback функцией. Наш таргет в данном случае - React ref
, прикрепленный к контейнеру div
.
Наша callback функция установит переменную состояния, указывающую, что изображение в зоне видимости. Затем функция вызовет 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 в браузере, вы можете увидеть это в действии. Загрузка фотогрфий происходит "лесенкой" или "водопадом".
Начнем с создания двух новых файлов в директории components
: image.js
и 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
добавим код 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;
}
Наш основной компонент ImageContainer
после добавления в него компонента Image
должен выглядеть следующим образом:
import React from "react";
import useIntersectionObserver from "../hooks/use-intersection-observer";
import "./image-container.css";
import Image from "./image";
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 && (
<Image src={props.src} thumb={props.thumb} alt={props.alt} />
)}
</div>
);
};
export default ImageContainer;
Теперь давайте запустим наше приложение в последний раз. Обязательно откройте devtools в браузере и сбросьте кеширование.
Мы сделали приложение React с отложенной загрузкой изображений. Наше приложение отображает изображения только после того, как они попадают в область просмотра. Он также постепенно отображает их, сначала показывая размытые миниатюры. ¬