September 9th, 2021

JavaScript: загрузка видимых изображений

Решил задачу «Загрузка видимых изображений» к подразделу 3.6 «Прокрутка» второй части учебника по JavaScript.

Дана HTML-страница с текстом и кучей картинок (на заданной странице картинок всего — десять штук, и каждая из этих картинок занимает довольно много места на диске). Автор HTML-страницы решил оптимизировать ее для пользователей, у которых медленный и/или дорогой интернет.

Для этого автор HTML-страницы придумал, что при загрузке этой HTML-страницы у пользователя в браузере сразу будут подгружаться только те картинки, которые сразу видны в области просмотра браузера. Картинки, находящиеся на заданной HTML-странице вне области просмотра браузера (выше или ниже области просмотра браузера), сразу не загружаются. Когда пользователь начнет прокручивать HTML-страницу вверх или вниз, в область просмотра браузера начнут попадать и другие картинки, которые должны загружаться, как только они попадут в область просмотра браузера.

Таким образом, HTML-страница загрузится быстрее при начальной загрузке в браузер. Если пользователь решит, что данная страница ему неинтересна, он может с нее уйти. При этом, если пользователь прочел только начало HTML-страницы и ушел, то большая часть картинок так и не попадет в область просмотра браузера и не загрузится, в результате получится еще и экономия трафика.

Задача состоит в том, чтобы написать скрипт на языке JavaScript, который обеспечит такое поведение HTML-страницы.

* * *

Как это предполагается реализовать? При постановке данной задачи даны конкретные указания, как это сделать. Также дана тестовая HTML-страница, ее можно посмотреть в песочнице. Задача может показаться непростой для ученика, но она очень проста, так как ученика практически ведут за ручку, как ребенка: на заданной HTML-странице уже присутствует часть решения, написанная на языке JavaScript. Нам нужно лишь добавить код в указанное место этого решения.

Все картинки в коде HTML-страницы представлены HTML-элементом img следующим образом (это лишь пример, а не одна из картинок):
<img src="placeholder.svg" width="128" height="128" data-src="real.jpg">

Файл placeholder.svg тоже дан в песочнице. Как я уже писал ранее, файл формата SVG представляет собой текстовый файл, в котором на языке, похожем на язык HTML, описано изображение векторной графики. Кроме векторной графики формат SVG позволяет делать еще множество интересных вещей, например, анимацию изображений.

В данном случае файл placeholder.svg используется в качестве временной заглушки (одно из значений слова «placeholder» в переводе на русский язык — «заглушка») на месте всех изображений на заданной HTML-странице. Наш будущий скрипт должен будет заменить эту заглушку на файл с изображением, как только заглушка попадет в область просмотра браузера. Откуда скрипт возьмет название файла с изображением для замены заглушки? Как видно выше, для этого в HTML-элемент img добавлено нестандартное свойство data-src, в котором хранится название файла с изображением.

Так как все изображения имеют разные высоту и ширину, нам заранее, до загрузки изображения, требуется знать высоту и ширину этого изображения, чтобы понять, когда оно попадет в область просмотра браузера. Для этого, как видно выше, в каждом HTML-элементе img указаны свойства width и height с шириной и высотой конкретного изображения в пикселях. Кроме того, эти свойства регулируют размеры (ширину и высоту) временной заглушки (векторного изображения из файла placeholder.svg) для конкретного «реального» изображения.

Векторное изображение из файла placeholder.svg представляет собой так называемый «троббер», он же «значок загрузки», «асинхронный индикатор выполнения» или «спиннер» (по-английски «throbber» или «loading icon»):
https://ru.wikipedia.org/wiki/Троббер
https://en.wikipedia.org/wiki/Throbber

Это значок (картинка), который обычно означает, что нужно некоторое время подождать, пока программа выполнит некое действие. Ниже показано, как примерно выглядит троббер (дословно в переводе с английского «пульсатор», потому что «лепестки» изображенной фигуры как бы переливаются или пульсируют, вращаются) на заданной HTML-странице (я не могу в здесь вставить файл формата SVG, потому что по правилам ЖЖ в посты можно вставлять лишь картинки в форматах GIF, JPG и PNG, поэтому я выбрал формат GIF и преобразовал исходную картинку из SVG в GIF):

https://ic.pics.livejournal.com/ilyachalov/24714720/111638/111638_original.gif

(Я не стал вставлять картинку прямо в пост, потому что она анимированная, а эта анимация мешает при чтении текста, отвлекает.)

Вот часть решения (скрипт на языке JavaScript), которая уже присутствует на заданной HTML-странице:
/**
 * Проверка видимости элемента (в видимой части страницы)
 * Достаточно, чтобы верхний или нижний край элемента был виден
 */
function isVisible(elem) {
    // ...ваш код...
}

function showVisible() {
    for (let img of document.querySelectorAll("img")) {
        let realSrc = img.dataset.src;
        if (!realSrc) continue;

        if (isVisible(img)) {
            // отключение кеширования
            // эта строка должна быть удалена в "боевом" коде
            realSrc += "?nocache=" + Math.random();

            img.src = realSrc;

            img.dataset.src = "";
        }
    }
}

window.addEventListener("scroll", showVisible);
showVisible();

Функция showVisible загружает картинки, видимые в области просмотра браузера на заданной HTML-странице на момент запуска этой функции. Загрузка картинок происходит путем подмены временной заглушки placeholder.svg в свойстве src HTML-элемента img на название файла картинки из нестандартного свойства data-src HTML-элемента img.

Для этого функция выбирает все HTML-элементы img с заданной HTML-страницы с помощью метода querySelectorAll, а затем перебирает эти HTML-элементы в цикле, проверяя для каждой картинки, видна она в области просмотра браузера или нет. Если у картинки нет нестандартного свойства data-src, то такая картинка пропускается с помощью ключевого слова continue.

Видимость каждой картинки проверяется с помощью функции isVisible. Эту функцию от нас и требуют написать.

Как видим из вышеприведенного кода, функция showVisible запускается при загрузке HTML-страницы (последняя строка кода) и при прокрутке заданной HTML-страницы (для этого эта функция повешена в качестве функции-обработчика для события scroll на HTML-странице).

Про строку вышеприведенного кода, которая отключает кэширование, я подробно писал в предыдущем посте.

Зачем нужно отключение кэширования на период написания кода? Я думаю, это довольно старый прием, который лично мне сейчас при решении этой задачи не нужен (но, всё-таки, было интересно о нем узнать). Наверное, авторы обсуждаемой задачи каждый раз при тестировании замеряли время загрузки заданной HTML-страницы, а если кэширование включено, то уже на второй загрузке заданной HTML-страницы картинки будут взяты из кэша браузера (они окажутся в кэше браузера при первой загрузке HTML-страницы) и замеры времени станут бесполезны, не будут показывать экономии трафика.

Во-первых, в моем браузере есть волшебная комбинация клавиш Ctrl+F5, которая инициирует перезагрузку HTML-страницы, запрещая браузеру пользоваться кэшем (браузер при этом загружает файлы с веб-сервера, а не из своего кэша). Можно при тестировании пользоваться этой комбинацией клавиш, а не писать код отключения кэширования в скрипте на JavaScript (с другой стороны, Ctrl+F5 не работает при прокрутке HTML-страницы, но это, однако же, не мешает разработке и тестированию в данном случае).

Во-вторых, в моем браузере среди инструментов разработчика (F12) есть инструмент «Network», в котором можно включить галку «Disable cache» (по-русски «Отключить кэш») и кэш браузера будет отключён, пока открыта панель инструментов разработчика на данной HTML-странице. Можно пользоваться этой возможностью вместо вышеупомянутой строки кода, отключающей кэш браузера. Кстати, рекомендую при решении этой задачи постоянно пользоваться инструментом «Network», он прекрасен (показывает, какие файлы загружены, откуда они взяты, из кэша или с исходного веб-сервера, можно включить фильтр по видам файлов (например, есть фильтр по картинкам), всё это в реальном времени, с замером времени загрузки). Это замечательный инструмент.

* * *

Итак, от нас требуется всего лишь написать функцию isVisible. Первым параметром ей передается HTML-элемент elem, относительно которого нам нужно определить, виден ли этот HTML-элемент в данный момент в области просмотра браузера или нет. Функция должна вернуть либо значение true, либо значение false. Пишем код:
function isVisible(elem) {
    // координаты HTML-элемента (относительно области просмотра)
    let rect = elem.getBoundingClientRect();
    // высота окна (клиентской части области просмотра браузера)
    let viewportHeight = document.documentElement.clientHeight;
    
    return ((rect.top > 0 && rect.top < viewportHeight) ||
            (rect.bottom > 0 && rect.bottom < viewportHeight));
}

Находим координаты верхней и нижней границ заданного HTML-элемента elem относительно верхнего левого угла области просмотра браузера с помощью метода getBoundingClientRect. Далее получаем высоту области просмотра браузера viewportHeight.

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

Вот, собственно, и всё. Задача решена.

При постановке задачи требовалась лишь проверка видимости по вертикали. Добавим в решение и проверку видимости по горизонтали (к примеру, эта проверка может понадобиться, если у нас есть ряд изображений, расположенных горизонтально рядом друг с другом без переноса на следующую строку, какая-то часть этих изображений может быть размещена за пределами области просмотра браузера справа или слева, при этом по вертикали все эти изображения будут попадать в область просмотра браузера). Дополним код (красным цветом я пометил дополнения):
function isVisible(elem) {
    // координаты HTML-элемента (относительно области просмотра)
    let rect = elem.getBoundingClientRect();
    // высота окна (клиентской части области просмотра браузера)
    let viewportHeight = document.documentElement.clientHeight;
    // ширина окна (клиентской части области просмотра браузера)
    let viewportWidth = document.documentElement.clientWidth;
    
    return (((rect.top > 0 && rect.top < viewportHeight) ||
             (rect.bottom > 0 && rect.bottom < viewportHeight)) &&
            ((rect.left > 0 && rect.left < viewportWidth) ||
             (rect.right > 0 && rect.right < viewportWidth)));
}

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

Учебник по JavaScript, ч.2: Интерфейсные события

Прочел третий раздел «Интерфейсные события» второй части («Браузер: документ, события, интерфейсы») учебника по JavaScript.

https://learn.javascript.ru

Часть 2. Браузер: документ, события, интерфейсы (в т.ч. 32 подраздела)

Разделы:

3. Интерфейсные события (6 подразделов)

3.1 Основы событий мыши
3.2 Движение мыши: mouseover/out, mouseenter/leave
3.3 Drag'n'Drop с событиями мыши
3.4 Клавиатура: keydown и keyup
3.5 События указателя
3.6 Прокрутка

Очень интересный раздел, довольно много интересных задач, которые я разобрал в своих постах (тут не только разборы задач, но и «реплики в сторону»):

1. JavaScript: подсказки к HTML-элементам
2. JavaScript: тонкости работы с событиями мыши
3. Правильная постановка задачи это половина ее решения
4. JavaScript: умная подсказка, разбор постановки задачи
5. Название тестового фреймворка Mocha
6. JavaScript: умная подсказка, подключение автоматических тестов
7. JavaScript: умная подсказка, пять автоматических тестов
8. JavaScript: умная подсказка, начало решения задачи
9. JavaScript: умная подсказка, окончание решения задачи
10. CSS: схлопывание внешних отступов
11. JavaScript: слайдер (ползунок)
12. JavaScript: событие mouseup за пределами HTML-элемента
13. JavaScript: mousedown, preventDefault, iframe
14. JavaScript: расставить героев на поле, постановка задачи
15. JavaScript: расставить героев на поле, решение задачи
16. GIMP, формат PNG, утилита pngcrush
17. Новая версия файла heroes.png
18. JavaScript: одновременное нажатие клавиш
19. JavaScript: бесконечная страница
20. JavaScript: кнопка вверх, кнопка вниз
21. JavaScript: предотвращение кэширования файла
22. JavaScript: загрузка видимых изображений