Что такое сайд эффекты в ES модулях и как они влияют на бандл?

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

В этой статье разберемся что такое сайд эффекты, рассмотрим поле sideEffects в package.json, и как его значение влияет на финальный бандл и на tree shaking.

Многие проекты используют webpack в качестве сборщика, который может уменьшить размер своих выходных бандлов с помощью встряхивания дерева (tree shaking, удаления мертвого кода). Однако встряхивание деревьев может работать эффективно только в том случае, если оно знает о побочных эффектах в вашем коде и в пакетах, которые вы используете. В этой статье разберемся, что такое побочные эффекты, почему webpack должен знать о них и что он делает с этими знаниями.

Что такое побочные эффекты?

Побочным эффектом (сайд эффект, side effect) в контексте модуля ECMAScript является код, который выполняет некоторое внешне видимое поведение (то есть поведение, видимое вне модуля) при загрузке модуля. Вот пример:
import { registerThing } from 'thing-registry';
const store = registerThing( THING_KEY, { /* ... */ } );
registerStore вызывается на верхнем уровне, что означает, что он будет запущен, как только модуль будет впервые импортирован. Эти изменения видны внешне, так как вещи модифицируются во внешнем хранилище, которое находится в другом модуле (thing-registry). Другие примеры побочных эффектов включают установку глобальных значений в window или добавление поведения браузера с помощью полифиллов (polyfills).
Однако, если бы это произошло внутри функции init, которая не вызывается при загрузке модуля, то это больше не было бы побочным эффектом:
import { registerThing } from 'thing-registry';
 
export function init() {
  const store = registerThing( THING_KEY, { /* ... */ } );
}
 
// `init` не вызывается на верхнем уровне модуля,
// и таким образом импорт этого модуля не вызывает сайд эффектов.
Объявление переменной или выполнение каких-либо изменений на верхнем уровне, которые влияют только на текущий модуль, также не является побочным эффектом, поскольку они содержатся в модуле:
import list from './list';
 
// не сайд эффект
let localVariable = [];
 
// тоже не сайд эффект
for ( const entry of list ) {
  localVariable.push( processListEntry( entry ) );
}

Влияние побочных эффектов на сборку

Большинство современных сборщиков реализуют tree shaking, при котором неиспользуемый код удаляется из финальных бандлов, так как в этом нет необходимости. Это становится важным в библиотеках, которые предлагают множество различных функций, поскольку потребители этой библиотеки могут использовать только небольшую ее часть и не хотят, чтобы их бандлы были больше, чем необходимо. Таким образом, пакеты и библиотеки должны предпринять шаги для обеспечения того, чтобы их действительно можно было правильно встряхнуть. Это возвращает нас к побочным эффектам. Как мы видели, побочные эффекты — это код, который запускается просто в силу импорта модуля и оказывает какое-то внешнее влияние. Это означает, что код не может быть вытряхнут деревом; Он должен работать, потому что он изменяет вещи за пределами модуля, которые могут понадобиться в другом месте. К сожалению, побочные эффекты трудно определить автоматически, и некоторые сборщики (например, webpack) ошибаются в сторону осторожности, предполагая, что каждый модуль потенциально имеет побочные эффекты. Это становится проблемой для index модулей, которые повторно экспортируют (ре-экспортируют) вещи из других модулей, поскольку это фактически означает, что все, что там есть, теперь должно быть объединено вместе:
// index.js
 
export { a, b } from './module1';
export { c, d, e } from './module2';
export { f } from './module3';
 
// Tree shaking не может быть применен, т.к. сборщик не знает
// имеют ли эти ре-экспортируемые модули сайд эффекты.

Рассказываем сборщикам о побочных эффектах

Поскольку сборщики не могут сами разобраться в побочных эффектах, нам нужно явно заявить о них. Это делается в package.json пакетов. Это означает, что объявление побочных эффектов является обязанностью пакета, и для пакетов, которые этого не делают, webpack, скорее всего, не сможет ничего стереть. Пользователи такого пакета могут в конечном итоге вытащить все это в свою сборку, даже если они используют только небольшую часть пакета, часто без простого способа обойти его. Так как же заявить о побочных эффектах? Лучший способ сделать это зависит от того, насколько вы их используете и где они находятся в вашем коде. Очень часто ваш пакет вообще не будет использовать никаких побочных эффектов. В этой ситуации вы можете просто установить sideEffects значение false:
{
  "name": "package",
  "sideEffects": false
}
Если в нем есть несколько файлов с побочными эффектами, вы можете перечислить их:
{
  "name": "package",
  "sideEffects": [ "dist/store.js", "dist/polyfill.js" ]
}
Это позволяет сборщику предполагать, что только те модули, которые были объявлены, имеют побочные эффекты, а ничто другое не имеет. Конечно, это означает, что мы должны быть осторожны, чтобы включить все, что имеет побочные эффекты, или проблемы могут возникнуть в приложениях, использующих пакет.
Webpack поддерживает ряд сложных функций сопоставления, поэтому другой подход, который вы можете использовать, заключается в инвертировании вещей и объявлении путей без побочных эффектов, оставляя webpack предполагать, что все остальное может иметь побочные эффекты:
{
  "name": "package",
  "sideEffects": [
    "!(dist/(components|utils)/**)"
  ]
}
Приведенный выше пример говорит сборщику, что он должен предполагать, что все, что находится за пределами каталогов components и utils, содержит побочные эффекты. Этот подход должен гарантировать, что все в components и utils может быть встряхнуто деревом без проблем с побочными эффектами и потенциально вызовет проблемы только в том случае, если один из файлов там использует побочные эффекты. В некоторых ситуациях может быть предпочтительнее пометить конкретный вызов функции как не имеющий побочных эффектов, а не весь файл. Рассмотрим следующий пример:
function noSideEffects() {
  // Do something.
}
 
noSideEffects();
Сборщик не может определить, содержит ли вызов верхнего уровня noSideEffects какие-либо побочные эффекты. Одним из решений было бы включение модуля в поле package.json sideEffects, как мы видели выше. Однако мы также можем обрабатывать его в коде с помощью подсказки PURE:
function noSideEffects() {
  // Do something.
}
 
/*#__PURE__*/ noSideEffects();

Потеря побочных эффектов у потребителя

Рассмотрим другой случай. Теперь вы являетесь потребителем и импортируете пакет, в котором используются побочные эффекты. Вы хотите их использовать. На самом деле, вы полагаетесь на то, что эти побочные эффекты произойдут, иначе ваш код не будет работать правильно. Вы можете столкнуться с некоторыми ситуациями, когда побочные эффекты неожиданно пропадают. Вот пример:
// index.js
 
import 'side-effectful-module';
 
export { a, b } from './impl';
// impl.js
 
function a() {
  // что-то делает
}
 
function b() {
  // делает что-то, что зависит от произошедших сайд эффектов
}
В index.js мы видим то, что часто называют “голым импортом” (naked import). Этот синтаксис означает, что мы не используем ни один из экспортов модуля, и нас фактически интересуют только его побочные эффекты. Голый импорт сам по себе не является побочным эффектом, но он является очень сильным сигналом о том, что побочные эффекты присутствуют во всем, что вы импортируете. index.js не делает ничего, кроме импорта side-effectful-module, просто ре-экспортируя функции из impl.js. Ключевым моментом здесь является то, что функции в impl.js на самом деле зависят от побочного эффекта, который происходит в index.js. Если встряхивание деревьев отключено, например, в режиме разработки, все будет работать правильно. Однако, если встряхивание дерева включено, модуль index.js может быть удален из дерева, оставив только фактические функции из impl.js. Если это произойдет, побочный эффект будет потерян, и b потерпит неудачу. То же самое может произойти, если импорт выполняется в дочернем модуле:
// index.js
 
import { unused } from './util';
 
function b() {
  // делает что-то, что зависит от произошедших сайд эффектов
}
 
// Этот модуль не использует функцию `unused`.
// util.js
 
import "side-effectful-module";
 
export function unused() {
  // делает что-то, что зависит от произошедших сайд эффектов
}
Поскольку unused не используется в index.js, он будет вытряхнут деревом. Это означает, что ничего из impl.js больше не нужно, поэтому ничего из этого не будет загружено. В очередной раз мы теряем побочный эффект, хотя он необходим.

Предотвращение побочных эффектов при встряхивании деревьев

Поскольку эти побочные эффекты по существу являются автоматически выполняемыми безымянными зависимостями, мы должны рассматривать их как таковые. Если в модуле есть код, который зависит от побочного эффекта, мы должны быть уверены, что импортируем его туда. Второй пример можно легко исправить с помощью дополнительного импорта в index.js:
// index.js
 
import "side-effectful-module";
import { unused } from './util';
 
function b() {
  // делает что-то, что зависит от произошедших сайд эффектов
}
 
// этот модуль все еще не использует `unused`.
Это гарантирует, что side-effectful-module будет выполняться перед любым кодом в index.js и не будет вытряхиваться из дерева. Обратите внимание, что теперь мы импортируем побочный эффект в оба модуля, но это нормально! Модули ES запускаются только один раз, а это означает, что их побочные эффекты также будут запускаться только один раз, независимо от того, сколько файлов они импортированы.

Подведем итоги

В итоге получилась тонна информации, поэтому давайте попробуем обобщить практические советы:
  • Если ваша библиотека не содержит побочных эффектов, установите sideEffects: false в package.json.
  • Если ваша библиотека содержит побочные эффекты, вы все равно можете включить встряхивания деревьев насколько это возможно. Перечислите файлы с побочными эффектами в явном виде или используйте обратные условия, чтобы перечислить пути, которые не имеют побочных эффектов.
  • Если вы полагаетесь на побочные эффекты от внешнего модуля, обязательно импортируйте этот модуль везде, где вы их используете.

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

год назад·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, чтобы она была видна при загрузке.

Итоги

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