СоХабр закрыт.
С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.
async
и defer
атрибутов). Кто-то даже сказал, что он попал в самый центр зла: мир scroll эвент-листенеров. Мы никогда не узнаем наверняка, что с ним случилось в итоге, но как бы то ни было, этот разработчик абсолютно вымышленный и сходство с любым другим разработчиком совершенно случайно.scroll
-мрака и поговорим о современных способах ленивой загрузки ресурсов. Не только о ленивой загрузке изображений, но и о любых других ассетах в этом плане. Более того, техника, которую мы сегодня обсудим способна на значительно большее, чем просто ленивая загрузка: мы сможем реализовать любой вид отложенной функциональности, базирующейся на видимости элементов для пользователей.IntersectionObserver
.IntersectionObserver
в частности, мы должны взглянуть на то как предполагается использовать в современной сети Observer-ы в целом./**
* Typical Observer's registration
*/
let observer = new YOUR-TYPE-OF-OBSERVER(function (entries) {
// entries: Array of observed elements
entries.forEach(entry => {
// Here we can do something with each particular entry
});
});
// Now we should tell our Observer what to observe
observer.observe(WHAT-TO-OBSERVE);
entries
— это массив значений, а не единичное значение.observe()
и аргументами, передаваемыми в функциях обратного вызова. Например, MutationObserver
должен также получить объект конфигурации, чтобы знать больше о том, за какими изменениями следить в DOM. PerformanceObserver
не следит за DOM-нодами, вместо этого у него есть предопределенный набор типов значений, за которыми он может наблюдать.IntersectionObserver
.IntersectionObserver
.Intersection Observer API позволяет веб-приложениям асинхронно следить за изменением пересечения элемента с его родителем или областью видимости документа viewport.Проще говоря,
IntersectionObserver
асинхронно следит за перекрытием одного элемента другим. Поговорим о том, какие элементы предназначены для IntersectionObserver
.IntersectionObserver
немного расширяет эту структуру. Во-первых, обсерверам этого типа требуется конфигурация из трех основных элементов:root
: Это корневой элемент, используемый для наблюдения. Он определяет базовую “область захвата” для наблюдаемых элементов. По умолчанию, root
— это viewport вашего браузера, но на самом деле это может быть любой элемент в вашем DOM(в этом случае вы устанавливаете root как что-то вроде document.getElementById('your-element')
). Имейте в виду, что в этом случае элементы, за которыми вы хотите наблюдать, должны находиться внутри DOM-дерева root
-элемента.root
-элемента, который расширяет или сжимает “область захвата”, когда размеры вашего root
-элемента не дают необходимой гибкости. Возможные варианты для этих конфигурационных значений похожи на значения margin
в CSS, такие как rootMargin: '50px 20px 10px 40px'
(верхний, правый, нижний левый). Можно использовать краткую форму записи(типа rootMargin: '50px'
) и выражать значения в px
или %
. По умолчанию rootMargin: '0px'
.root
и rootMargin
) моментально. threshold
задает процентное значение от такого пересечения, на которое обсервер должен реагировать, Оно может быть задано как единичное значение или как массив значений. Чтобы лучше понять, какой эффект производит threshold
(я понимаю, что это иногда может сбивать с толку), посмотрим на несколько примеров:threshold: 0
: IntersectionObserver
с этим значением по-умолчанию должен реагировать, когда самый первый пиксель наблюдаемого элемента пересечет одну из границ “области захвата”. Заметьте! IntersectionObserver
отреагирует на оба варианта: a) когда элемент входит и b) когда элемент покидает “область захвата”threshold: 0.5
: Обсервер должен сработать, когда 50% от наблюдаемого элемента пересекает “область захвата”threshold: [0, 0.2, 0.5, 1]
: Обсервер должен реагировать в четырех случаях:IntersectionObserver
);threshold: 0
IntersectionObserver
нашу желаемую конфигурацию, мы просто передаем наш объект config
в конструктор обсервера вместе с функцией обратного вызова, например так:const config = {
root: null, // avoiding 'root' or setting it to 'null' sets it to default value: viewport
rootMargin: '0px',
threshold: 0.5
};
let observer = new IntersectionObserver(function(entries) {
…
}, config);
IntersectionObserver
реальный элемент для наблюдения. Это достигается простой передачей элемента в функцию observe()
:…
const img = document.getElementById('image-to-observe');
observer.observe(image);
root
некий DOM-элемент, наблюдаемый элемент должен находиться внутри DOM-дерева этого root
-элемента.IntersectionObserver
может принять лишь один элемент для наблюдения за раз и не поддерживает массовую установку наблюдателей. Это означает, что если вам нужно следить за несколькими элементами(скажем, несколько изображений на странице), вам придется проитерировать их по списку и наблюдать каждый из них по-отдельности:…
const images = document.querySelectorAll('img');
images.forEach(image => {
observer.observe(image);
});
IntersectionObserver
вызван по одному разу для всех наблюдаемых элементов. Даже для тех, которые не соответствуют заданной конфигурации. “Ну, это не совсем то, чего я ожидал”- это вполне обычная мысль, когда сталкиваешься с этим впервые. Но не запутайтесь: это необязательно значит, что эти наблюдаемые элементы как-то пересекают “область захвата” во время загрузки страницы.IntersectionObserver
. Это может добавить ненужный шум к вашей функции обратного вызова и вы становитесь ответственным за выявление элементов, действительно пересекающих “область захвата” и элементов, которые пока не нужно учитывать. Чтобы понять, как же их выявлять, погрузимся немного в анатомию нашего колбека и посмотрим, как устроены подобные вещи.IntersectionObserver
принимает на вход два аргумента, и мы поговорим о них в обратном порядке, начиная со второго аргумента. Вместе с вышеупомянутым массивом наблюдаемых элементов, пересекающих “область захвата”, данный колбек получает доступ к самому обсерверу через второй аргумент.new IntersectionObserver(function(entries, SELF) {…});
IntersectionObserver
в первый раз. Сценарии, вроде ленивой загрузки изображений, отложенной загрузки прочих ассетов и т.д. На тот случай, когда вы хотите перестать наблюдать за элементом, IntersectionObserver
предоставляет метод unobserve(element-to-stop-observing), который можно вызвать в функции обратного вызова после того, как вы произвели какие-либо действия над наблюдаемым элементом(например, как в случае с ленивой загрузкой изображения).new IntersectionObserver(function(ENTRIES, self) {…});
entries
, который мы получаем в нашем колбеке — это массив, состоящий из элементов специального типа: IntersectionObserverEntry. Данный интерфейс предоставляет нам предопределенный и заранее вычисленный набор свойств для каждого конкретного наблюдаемого элемента. Посмотрим на самые интересные из них.IntersectionObserverEntry
поступают в наше распоряжение с информацией о трех различных прямоугольниках — определяющих координаты и границы элементов, вовлеченных в процесс:root
+ rootMargin
)getBoundingClientRect()
, offsetTop
, offsetLeft
и других дорогих свойств и методов, связанных с позиционированием и триггерящих layout thrashing. Чистая победа в плане производительности!IntersectionObserverEntry
, которое нас интересует — это isIntersecting
. Это удобное свойство, показывающее пересекает ли наблюдаемый элемент “область захвата” в данный момент или нет. Конечно, мы могли бы получить эту информацию с помощью intersectionRect
(если прямоугольник не 0x0, значит элемент пересекает “область захвата”), но иметь такое свойство вычисленным заранее очень удобно.isIntersecting
можно использовать, чтобы выяснить, входит ли наблюдаемый элемент в “область захвата” или уже покидает ее. Чтобы это выяснить, сохраните значение этого свойства как глобальный флаг и когда вы получите в колбек новый экземпляр IntersectionObserverEntry
, сравните новое значение isIntersecting
с глобальным флагом:false
, а теперь true
значит элемент входит в “область захвата”false
, но было true
до этого, значит элемент покидает “область захвата”isIntersecting
это именно то свойство, которое поможет нам ранее упомянутую проблему, то есть отделение экземпляров IntersectionObserverEntry
для элементов, которые действительно пересекают “область захвата” от шума создаваемого элементами, которые были лишь инициализированы.let isLeaving = false;
let observer = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
// we are ENTERING the "capturing frame". Set the flag.
isLeaving = true;
// Do something with entering entry
} else if (isLeaving) {
// we are EXITING the "capturing frame"
isLeaving = false;
// Do something with exiting entry
}
});
}, config);
isIntersecting
не было реализовано и возвращает undefined
, несмотря на полную поддержку IntersectionObserver
во всех остальных отношениях. Оно было поправлено в июле 2017 и доступно начиная с Edge 16.IntersectionObserverEntry
предоставляет еще одно заранее вычисленное свойство: intersectionRatio
. Этот параметр можно использовать для тех же целей, что и isIntersecting
, но он дает значительно больший контроль и точность, будучи числом с плавающей точкой, а не булевым значением. Значение intersectionRatio
показывает насколько наблюдаемый элемент пересекает “область захвата”(отношение intersectionRect
к boundingClientRect
). Опять же, мы могли бы и сами посчитать это, используя информацию о наших прямоугольниках, но то, что это сделано за нас — просто отлично.intersectionRatio
похоже на threshold
объекта конфигурации обсервера. Разница в том, что последний определяет, когда вызвать обсервер, в то время как второй срабатывает при реальном пересечении(это слегка отличается от threshold
из-за асинхронной природы обсерверов). target
— еще одно свойство интерфейса IntersectionObserverEntry
, которое может понадобиться вам достаточно часто. Но здесь нет совершенно никакой магии — только оригинальный элемент, переданный в функцию observe()
вашего обсервера. Вроде event.target
, который вы используете, работая с событиями.IntersectionObserverEntry
, обратитесь к спецификации.IntersectionObserver
. Скажем, у вас есть элемент, который должен производить кучу вычислений будучи показанным на экране. Например, показ вашей рекламы должен быть зарегистрирован только когда она была действительно показана пользователю. Но сейчас, представим, что у вас есть элемент-карусель с автовоспроизведением где-то под первым экраном на вашей странице:isIntersecting
интерфейса IntersectionObserverEntry
.const carousel = document.getElementById('carousel');
let isLeaving = false;
let observer = new IntersectionObserver(function(entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
isLeaving = true;
entry.target.startCarousel();
} else if (isLeaving) {
isLeaving = false;
entry.target.stopCarousel();
}
});
}
observer.observe(carousel);
config
, передаваемый для инициализации IntersectionObserver
: это значит, что мы полагаемся на дефолтные значения конфигурации. Когда карусель покидает в наш вьюпорт, нам следует остановить ее воспроизведение и не тратить ресурсы на элементы, которые больше не важны.IntersectionObserver
: мы не хотим тратить ресурсы, чтобы скачать что-то, что не нужно пользователю прямо сейчас. Это даст большое преимущество вашим пользователям: пользователям не нужно будет скачивать и их мобильные устройствам не нужно будет парсить и компилировать кучу информации, которая им в данный момент не нужна. Неудивительно, что это также улучшит производительность вашего приложения.scroll
. Проблема очевидна: это триггерит обработчики событий слишком часто. Поэтому мы вынуждены были изобрести throttling и debouncing, ограничивающие выполнения колбека. Но все это добавило нагрузки на основной поток тогда, когда мы так в нем нуждались.IntersectionObserver
для ленивой загрузки, на что мы должны обратить внимание? Давайте попробуем вживую простой пример ленивой загрузки изображений.…
<img data-src="https://blah-blah.com/foo.jpg">
…
src
: как только браузер встречает атрибут src
, он начинает загрузку изображения, что прямо противоположно нашим намерениям. Следовательно, нам нужно не задавать этот аттрибут для наших изображений в HTML и использовать вместо этого, например data-
аттрибут, вроде data-src
в нашем случае.const images = document.querySelectorAll('[data-src]');
const config = { … };
let observer = new IntersectionObserver(function (entries, self) {
entries.forEach(entry => {
if (entry.isIntersecting) { … }
});
}, config);
images.forEach(image => { observer.observe(image); });
data-src
аттрибутами;config
: для данного сценария мы хотим расширить “область захвата” чтобы отлавливать элементы чуть ниже нижней границы вьюпорта;IntersectionObserver
с этим конфигом;IntersectionObserver
;entries.forEach(entry => {
if (entry.isIntersecting) { … }
});
data-src
в настоящее изображение вида 
.if (entry.isIntersecting) {
preloadImage(entry.target);
…
}
preloadImage()
— это очень простая функция, о которой тут можно не упоминать. Просто прочитайте исходники.unobserve()
для уже обработанного изображения. Таким же образом, как мы вызываем removeEventListener()
для обычных событий, когда обработчики больше не нужны, чтобы предотвратить утечки памяти в нашем коде.if (entry.isIntersecting) {
preloadImage(entry.target);
// Observer has been passed as self to our callback
self.unobserve(entry.target);
}
unobserve(event.target)
мы также могли бы вызвать disconnect()
: это полностью отключит наш IntersectionObserver
и отменит отслеживание изображений насовсем. Это полезно, если все что вам нужно — это отследить первое срабатывание вашего обсервера. В нашем случае нужно, чтобы обсервер продолжал следить за изображениями.min-height: 100px;
Это сделано, чтобы задать плейсхолдеру изображения некоторый вертикальный размер( без src
атрибута). Для чего?inline-block
по умолчанию, все эти 0х0 блоки расположились бы бок о бок в одну линиюIntersectionObserver
зарегистрировал бы все(или, в зависимости от того как быстро вы скроллите, почти все) изображения сразу, не обеспечивая оптимальных результатовIntersectionObserver
— это намного больше, чем просто ленивая загрузка. Вот другой пример замены события scroll
с помощью этой технологии. В нем мы сталкиваемся с довольно обычным сценарием: нам нужно подсвечивать текущую секцию в навигационном меню в зависимости от позиции скролла в документе.config
:const config = { rootMargin: '-50px 0px -55% 0px' };
0px
для rootMargin
? Ну, просто потому что подсветка текущей секции и ленивая загрузка изображения довольно отличаются от того, что мы пытаемся достичь. В случае с ленивой загрузкой мы хотим начать загрузку до того, как изображение попадет во вьюпорт. Следовательно, для этой цели мы расширили нашу “область захвата” на 50px вниз. Напротив, если мы хотим подсветить текущую секцию, мы должны быть уверены, что секция действительно видна на экране. И не только в этом: мы должны быть уверены, что пользователь, на самом деле, читает или собирается прочитать именно эту секцию. Следовательно, мы хотим, чтобы секция появилась чуть более, чем наполовину от вьюпорта снизу перед тем как мы могли бы сказать, что эта секция является активной. Мы также хотим учесть высоту навигационной панели, поэтому мы отнимает высоту панели от “области захвата”.IntersectionObserver
наготове, следовательно вы не найдете здесь ни disconnect()
, ни unobserve()
.IntersectionObserver
— это крайне простая технология. Она достаточно неплохо поддерживается современными браузерами и если вы хотите реализовать ее для браузеров, которые пока не поддерживают ее(или уже никогда не будут), то конечно же для нее имеется полифил. Но в целом, это великолепная технология, позволяющая нам делать любые вещи, связанные с детектированием элементов во вьюпорте, в то же время помогая получить действительно хороший прирост производительности.IntersectionObserver
— это асинхронный неблокирующий APIIntersectionObserver
заменяет дорогие обработчики scroll и resize событийIntersectionObserver
производит все дорогостоящие вычисления, вроде getClientBoundingRect()
для вас, так что вам этого делать не надоIntersectionObserver
следует структурному паттерну других обсерверов, поэтому теоретически должен быть легко понятен, если вы знакомы с работой других обсерверовwindow.addEventListener('scroll')
, откуда все это пришло, будет сложно найти у данного обсервера какие-то минусы. Поэтому, просто запомним некоторые вещи:IntersectionObserver
— асинхронное неблокирующее API. И это здорово! Но крайне важно понимать, что код, выполняемый внутри ваших колбеков не будет выполняться асинхронно по умолчанию, несмотря на то что API асинхронный. Поэтому все еще есть шанс потерять все преимущества IntersectionObserver
в случае, если вычисления в вашем колбеке нагружают основной поток. Но это уже другая история.IntersectionObserver
для ленивой загрузки ассетов(типа изображений, к примеру), запустите .unobserve(asset)
после того, как ассет загружен.IntersectionObserver
способен детектировать пересечения только для элементов, затрагивающих структуру форматирования документа. Чтобы было понятнее: наблюдаемые элементы должны генерировать блок и как-то влиять на лейаут. Вот несколько примеров, чтобы объяснить получше:display: none
не может быть и речи;opacity: 0
или visibility: hidden
создают блок(даже если и не видны), поэтому они детектируются;width: 0;
height: 0;
подойдут. Хотя нужно заметить, что абсолютно спозиционированные элементы, спозиционированные за пределами границ элемента-родителя(с отрицательными значения margin
, top
, left
и т.д.) и обрезанные родителями с помощью overflow: hidden;
не будут задетектированы: их блоки за пределами структуры форматирования документа.
комментарии (4)