Вы когда-нибудь задумывались, как Medium загружает изображения? Возможно, вы заметили, как изображения отображаются в несколько этапов. В самом начале на экране появляется размытая версия изображения, которая затем заменяется полноразмерной версией.

Как Medium Stories загружает изображения

  • Загрузка изображения не начинается, пока изображение не попадет в область просмотра.
  • Затем загружается «размытый» эскиз.
  • Затем загружается полноразмерное изображение и заменяет эскиз.

Мы можем разделить эту технику загрузки изображений на две отдельные функции.

1. Ленивая загрузка (Lazy loading)

Ленивая загрузка - это метод, который откладывает загрузку некритических ресурсов во время загрузки страницы. Вместо этого эти некритические ресурсы загружаются в момент необходимости. Что касается изображений, «некритичный» часто является синонимом «вне экрана».— developers.google.com

2. Размытие картинки

Чтобы что-то отобразить на экране быстрее, мы показываем размытое крошечное изображение, масштабированное до полной ширины. Когда мы закончим загрузку полноразмерного изображения, мы меняем их местами.

Если вы раньше работали с 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 со стилями для нашего компонента.

5

В компоненте 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) и посмотреть, как оно выглядит.

1

Теперь у нас есть несколько блоков того же размера, что и изображения, которые мы хотим визуализировать.

Intersection Observer

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;

Теперь мы визуализируем полноразмерное изображение, когда наш компонент входит в область просмотра. В действии это выглядит так.

2

Наше приложение теперь “лениво” загружает изображения. Наши изображения загружаются только тогда, когда они видны в области просмотра. Если вы проверите вкладку Network в браузере, вы можете увидеть это в действии. Загрузка фотогрфий происходит “лесенкой” или “водопадом”.

3

Добавление техники размытия (Blur - эффект)

Начнем с создания двух новых файлов в директории 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 в браузере и сбросьте кеширование.

4

Мы сделали приложение React с отложенной загрузкой изображений. Наше приложение отображает изображения только после того, как они попадают в область просмотра. Он также постепенно отображает их, сначала показывая размытые миниатюры.

Источник