Как ускорить сайт с помощью ленивой загрузки изображений

год назад·1 мин. на чтение

Ленивая загрузка изображений — один из самых простых способов ускорить загрузку сайта, поскольку для самой простой реализации ленивой загрузки требуется всего одна строка кода.

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

Что такое ленивая загрузка?

Ленивая загрузка (отложенная загрузка) — это метод, используемый для отсрочки загрузки контента до тех пор, пока он не понадобится. В случае изображений это означает, что изображение не будет загружено до тех пор, пока пользователь не прокрутит до точки, где изображение будет видно на экране. Это отличный способ ускорить работу вашего сайта, поскольку вы загружаете только те изображения, которые пользователь действительно увидит. Это особенно полезно для сайтов с большим количеством изображений, поскольку вы можете сэкономить много пропускной способности, загружая только те изображения, которые пользователь действительно увидит. Если у вас высокая скорость интернета или вы просматриваете сайты только с небольшими, хорошо оптимизированными изображениями, вы можете не увидеть преимущества отложенной загрузки изображений, поскольку вы можете загрузить все изображения почти мгновенно. Но для всех остальных ленивая загрузка изображений меняет играет важную роль. Это касается не только людей со сверхмедленным интернет-соединением. Изображения являются одним из, если не самым большим по размеру контентом, который загрузит ваш пользователь, поэтому, даже если у него быстрое подключение к Интернету, ленивая загрузка изображений все равно может иметь огромное значение для времени загрузки вашего сайта.

Базовая реализация ленивой загрузки

Как я уже упоминал в начале этой статьи, ленивая загрузка изображений так же проста, как добавление одного атрибута к тегу изображения. Атрибуту loading можно присвоить значение lazy, чтобы включить отложенную загрузку изображения. Браузер автоматически определит, когда загружать изображение, в зависимости от того, насколько близко изображение находится на экране.
<img src="image.jpg" loading="lazy" />
Самым большим недостатком этой базовой отложенной загрузки является то, что пользователь увидит пустое место, где должно быть изображение, пока изображение не будет загружено. Это не идеальный пользовательский опыт, поэтому оставшаяся часть этой статьи покажет вам, как воспользоваться отложенной загрузкой, чтобы показать размытое изображение-заполнитель до тех пор, пока не будет загружено полное изображение.

Продвинутая отложенная загрузка

Размытые изображения-заполнители отображаются до тех пор, пока не будет загружено полное изображение, и являются первым шагом к созданию этого расширенного эффекта отложенной загрузки. Чтобы создать размытое изображение-заполнитель, вам просто нужно создать версию изображения со сверхнизким разрешением. Есть много способов сделать это, например, использовать такой сервис, как BlurHash, вручную изменить размер изображения в таком инструменте, как Figma, или автоматически с помощью такого инструмента, как ffmpeg. Мы будем использовать ffmpeg для создания изображений-заполнителей для этой статьи, поскольку это наиболее гибкий вариант, который можно легко автоматизировать. Все, что нужно сделать, это запустить приведенный ниже код в командной строке в каталоге, содержащем изображение, для которого требуется сгенерировать изображение-заполнитель.
ffmpeg -i imageName.jpg -vf scale=20:-1 imageName-small.jpg
При этом будет сгенерировано изображение шириной 20 пикселей, а высота будет автоматически рассчитана для сохранения пропорций исходного изображения. Вы можете изменить ширину на любую другую, но по наблюдениям, 20 пикселей хорошо подходят для большинства изображений и достаточно малы, чтобы загружаться почти мгновенно даже при медленном интернет-соединении. Изображения-заполнители будут примерно по 1КБ каждое.
Следующим шагом является создание div и установка фонового изображения этого div на наше супер маленькое изображение. Это будет изображение-заполнитель, которое будет отображаться до тех пор, пока не будет загружено полное изображение. Наш код будет выглядеть примерно так.
<div class="blurred-img"></div>
.blurred-img {
  background-image: url(imageName-small.jpg);
  background-repeat: no-repeat;
  background-size: cover;
}
Этот div с blurred-img имеет размер в зависимости от размера содержимого в нем. Однако мы можем легко исправить это, добавив img в наш div и убедившись, что он скрыт по умолчанию, чтобы мы никогда не видели его в наполовину загруженном состоянии.
<div class="blurred-img">
  <img src="imageName.jpg" loading="lazy" />
</div>
.blurred-img img {
  opacity: 0;
}
Это даст нам эффект, который мы ищем. Эффект размытия, который мы получаем автоматически, связан с тем, что сверхмаленькое изображение автоматически увеличивается браузером. Если вы хотите добавить больше размытия, вы всегда можете использовать свойство CSS filter, чтобы добавить фильтр к blurred-img.
.blurred-img {
  filter: blur(10px);
}
Вы даже можете сделать еще один шаг вперед, добавив пульсирующую анимацию к изображению-заполнителю. Это сделает еще более очевидным, что изображение загружается.
.blurred-img::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: pulse 2.5s infinite;
  background-color: white;
}

@keyframes pulse {
  0% {
    opacity: 0;
  }
  50% {
    opacity: 0.1;
  }
  100% {
    opacity: 0;
  }
}
Теперь единственное, что осталось сделать, это показать основное изображение после его загрузки. Это немного сложнее, чем остальная часть кода, который мы написали до сих пор, поскольку требует от нас использования JavaScript, но все же довольно просто. Нам просто нужно добавить прослушиватель событий к изображению, который будет срабатывать после загрузки изображения.
<div class="blurred-img">
  <img src="imageName.jpg" loading="lazy" />
</div>
const blurredImageDiv = document.querySelector(".blurred-image")
const img = blurredImageDiv.querySelector("img")
function loaded() {
  blurredImageDiv.classList.add("loaded")
}

if (img.complete) {
  loaded()
} else {
  img.addEventListener("load", loaded)
}
.blurred-img {
  background-repeat: no-repeat;
  background-size: cover;
}
.blurred-img::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: pulse 2.5s infinite;
  background-color: var(--text-color);
}

@keyframes pulse {
  0% {
    opacity: 0;
  }
  50% {
    opacity: 0.1;
  }
  100% {
    opacity: 0;
  }
}

.blurred-img.loaded::before {
  animation: none;
  content: none;
}

.blurred-img img {
  opacity: 0;
  transition: opacity 250ms ease-in-out;
}

.blurred-img.loaded img {
  opacity: 1;
}
Здесь много кода, поэтому разберем его шаг за шагом. В коде JavaScript мы выбираем blurred-img а затем выбираем img в этом div. Затем мы проверяем свойство complete у img, чтобы увидеть, загрузился ли он еще. Если это так, это означает, что изображение уже загружено, поэтому мы можем просто вызвать функцию loaded. Однако, если это условие ложно, нам нужно добавить прослушиватель событий в img, который будет срабатывать после загрузки изображения, а затем вызывать loaded. loaded просто добавляет класс loaded в blurred-img. В CSS у нас есть несколько изменений в коде. Сначала мы удалили animation/content из элемента blurred-img::before. Это остановит пульсирующую анимацию после загрузки изображения. Мы также добавили transition к элементу img, чтобы он плавно исчезал при добавлении loaded класса в div blurred-img.img Наконец, мы изменяем непрозрачность img на 1, чтобы она была видна при загрузке.

Итоги

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

Что такое функтор? Функциональное программирование

2 года назад·5 мин. на чтение

В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Функтор.

Это серия статей о функциональном программировании:
  1. Парадигмы программирования
  2. Композиция
  3. Функторы (рассматривается в этой статье)
  4. Каррирование
  5. Чистые функции
  6. Функции первого класса

Что такое функтор?

Функтор (functor) это:
  • обертка над значением,
  • предоставляет интерфейс для преобразование (map),
  • подчиняется законам функтора (поговорим о них позже).

Примеры функторов

  • Массив (Array),
  • Промис (Promise).

Почему массив - функтор?

Вспомним определение функтора:
  • обертка над списком значений,
  • предоставляет интерфейс для преобразования - метод map,
  • подчиняется законам функтора.
[1, 2, 3]      // обернутое значение
  .map(        // интерфейс для преобразования значения
    x => x * 2
  )

Почему промис - функтор?

Промис это:
  • обертка над любым значением из JavaSctipt типов,
  • предоставляет интерфейс для преобразования - метод then,
  • подчиняется законам функтора.
const promise = new Promise((resolve, reject) => {
  resolve(
    { data: "value" } // обернутое значение, в данном случае объект
  )
});
 
promise
  .then(              // интерфейс для преобразования значения
    response => console.log(response)
  );

Что объединяет массив (или промис) и функтор?

Функтор - это паттерн проектирования, а Array и Promise - типы данных, которые основаны на этом паттерне.

Почему мы говорим, что массив и промис - функторы?

Чтобы понять, что функторы ближе чем кажутся. Массив и промис легко понять, при это они являются мощной концепцией. Мы используем их ежедневно, даже не подозревая об их сущности.

Где использовать функторы?

Немного поговорив о функторах и связав их с нашим повседневным использованием, было бы разумно рассмотреть их подробнее. Чтобы лучше понять идею функтора, создадим свои собственные функторы. Для начала рассмотрим такую задачу. Предположим, есть следующий кусочек данных.
{
  products: [
    {
      name: "All about JavaScript",
      type: "book",
      price: 22,
      discount: 20
    }
  ]
}

Постановка задачи

Найти финальную цену первого товара с учетом скидки. Если по какой-либо причине будут переданы неправильные данные, вывести строку "No data".

Шаги алгоритма

  1. Найти первый продукт со скидкой,
  2. Применить скидку,
  3. Продолжать проверку данных на валидность. Если данные не валидны - вернуть "No data".

Традиционное решение

const isProductWithDiscount = product => {
  return !isNaN(product.discount)
}
const findFirstDiscounted = products => {
  products.find(isProductWithDiscount)
}
 
const calcPriceAfterDiscount = product => {
  return product.price - product.discount
}
 
const findFinalPrice = (data, fallbackValue) => {
  if(!data || !data.products) return fallbackValue
 
  const discountedProduct = findFirstDiscounted(data.products)
  if(!discountedProduct) return fallbackValue
 
  return calcPriceAfterDiscount(discountedProduct)
}
 
findFinalPrice(data, "No data")

Комментарии к традиционному решению

Достоинства:
  • Атомарные логические единицы (isProductWithDiscount, findFirstDiscounted и calcPriceAfterDiscount),
  • Логику защищена от невалидных данных.
Что можно улучшить:
  • Cлишком много защитных проверок. (Защитное программирование (Defensive programming) является обязательным в любом отказоустойчивом программном обеспечении. Однако, в нашем коде 50% тела функции findFinalPrice — проверка на валидность данных. Это слишком много).
  • fallbackValue почти везде.

Почему нас волнуют эти улучшения?

Потому что данный код заставляет слишком в него вникать. Это негативно влияет на DX (Developer Experience) - уровень удовлетворенности разработчика от работы с кодом. Проанализируем код, чтобы прийти к лучшему решению. Части, которые мы стремимся улучшить, формируют паттерны (защита (defence) и откат (fallback)). Хорошо то, что эти части на самом деле цельные и атомарные. Мы должны иметь возможность абстрагировать этот паттерн в оболочку, которая могла бы обрабатывать эти крайние случаи вместо нас. Обертка позаботится о крайних случаях, а нам останется позаботиться только о бизнес-логике.

Функтор Maybe

Как мы обсуждали ранее, нам нужна только обертка, которая абстрагируется от обработки данных. Итак, роль функтора Maybe состоит в том, чтобы обернуть наши данные (потенциально невалидные данные) и обработать для нас крайние случаи.

Имплементация функтора Maybe

function Maybe(value) {
  const isNothing = () => {
    return value === null || value === undefined
  }
  
  const map = (fn) => {
    return isNothing() ? Maybe() : Maybe(fn(value))
  }
 
  const getValueOrFallback = {
    return (fallbackValue) => isNothing() ? fallbackValue : value;
  }
 
  return {
    map,
    getValueOrFallback,
  };
}

Пояснения к имплементации

  • isNothing проверяет валидно ли обернутое в функтор Maybe значение
  • map - интерфейс для преобразования обернутого значения, с помощью которого мы применяем функции с бизнес логикой к обернутому значению. map возвращает новое значение в другом экземпляре Maybe. Таким образом, мы можем сделать цепочку вызовов map - .map().map().map....
  • getValueOrFallback возвращает обернутое значение или запасное значение fallbackValue.

Как использовать функтор Maybe?

С валидными данными:
Maybe('Hello')
  .map(x => x.substring(1))
  .getValueOrFallback('fallback') // 'ello'
С невалидными данными:
Maybe(null)
  .map(x => x.substring(1))       // функция не будет запущена
  .getValueOrFallback('fallback') // 'fallback'
Функтор Maybe обработал крайние случаи вместо нас и не запустил функцию с невалидными данными. Нам нужно лишь позаботиться о бизнес логике. Таким образом, мы внедрили улучшение, о котором говорили в традиционном решении. Внедрим это решение в задачу.

Решение задачи с функтором Maybe

const isProductWithDiscount = product => {
  return !isNaN(product.discount)
}
const findFirstDiscounted = products => {
  return products.find(isProductWithDiscount)
}
const calcPriceAfterDiscount = product => {
  return product.price - product.discount
}
 
Maybe(data)
 .map((x) => x.products)
 .map(findFirstDiscounted)
 .map(calcPriceAfterDiscount)
 .getValueOrFallback("No data")

Комментарии к решению с функтором Maybe

Мы смогли улучшить традиционное решение при помощи функтора Maybe:
  • мы не защищаем код сами, вместо нас это делает функтор Maybe,
  • мы указали fallbackValue только один раз.
Как функтор Maybe соответствует определению функтора? Функтор Maybe это:
  1. обертка над любым значением из JavaScript типов,
  2. предоставляет интерфейс для преобразования - метод map,
  3. подчиняется законам функтора.

Законы функторов

Закон идентичности (Identity law)

Если при выполнении операции преобразования, значения в функторе преобразовываются сами на себя, результатом будет немодифицированный функтор.
const m1 = Maybe(value)
const m2 = Maybe(value).map(v => v)
// m1 и m2 эквивалентны

Закон композиции (Composition law)

Если две последовательные операции преобразования выполняются одна за другой с использованием двух функций, результат должен быть таким же, как и при одной операции отображения с одной функцией, что эквивалентно применению первой функции к результату второй.
const m1 = Maybe(value).map(v => f(g(v)))
const m2 = Maybe(value).map(v => g(v)).map(v => f(v))
// m1 и m2 эквивалентны

Зачем использовать функторы?

  • Абстракция над применением функции,
  • Усиление композиции функций,
  • Уменьшение количества защитного кода (как в функторе Maybe),
  • Более чистая структура кода,
  • Переменные более явно указывают на то, что мы ожидаем (что Maybe моделирует значение, которое может присутствовать, а может и не присутствовать).

Что означает Абстракция над применением функции?

То, что мы передаем функцию (т.е. x => x.products) в интерфейс преобразования (т.е. map) обертки (т.е. Maybe), и она знает, как позаботиться о себе (посредством своей внутренней реализации). Нас не интересуют детали реализации оболочки, которые она содержит (детали реализации скрыты), и тем не менее мы знаем, как использовать обертку (Array или Promise), используя их интерфейсы преобразования (map). И это на самом деле крайне важно в мире программирования. Это снижает планку того, как много мы, как программисты, должны понимать, чтобы иметь возможность что-то сделать. Функторы могут быть реализованы на любом языке, поддерживающем функции высшего порядка (а таких в наши дни большинство).

Почему функторы не используются повсеместно?

Просто потому, что мы к ним не привыкли. До .map.then) мы мутировали массивы или перебирали их значения вручную. Но как только мы обнаружили .map, мы начали адаптировать его в качестве нового инструмента преобразования. Я надеюсь, что, поняв ценность функторов, мы начнем чаще внедрять их в наши ежедневные задачи как привычный инструмент. Функтор Maybe - лишь пример функтора. Существует множество функторов, которые выполняет различные задачи. В этой статье мы рассмотрели самый простой из них, чтобы понять саму идею функторов.

Итоги

Функтор как паттерн проектирования - это простой, но очень мощный паттерн. Мы используем его ежедневно в различных типах данных, не догадываясь об этом. Было бы здорово, если мы сможем распознавать и ценить функторы немного больше и выделять им больше места в кодовой базе, потому что они делают код чище и дают нам больше возможностей.