loading="lazy" кажется универсальным ответом: поставил атрибут — картинка не грузится, пока не понадобится. На деле браузер решает «понадобится или нет» по расстоянию до экрана, и у этого решения есть слепое пятно: элементы, скрытые через display: none.
Это двадцать вторая часть цикла «PageSpeed без мифов».
Что происходит
У скрытого через display: none элемента нет положения на странице. Браузер не может вычислить, далеко ли он от экрана, — и в таких случаях Chrome загружает ленивую картинку сразу, как обычную. Где это встречается:
- мобильное меню с картинками или логотипами — скрыто, пока не нажали бургер;
- вкладки — все, кроме активной;
- фильтры и аккордеоны в свёрнутом состоянии;
- десктопные блоки, спрятанные на телефоне (и наоборот).
На одном из сайтов картинка в скрытом мобильном меню добавляла 27 КБ в окно первой отрисовки на каждой странице. Немного — но это вес, который не нужен ни одному посетителю, пока он не открыл меню.
Отдельный случай: декор, которого никто не видит
Ещё хуже — картинки, которые скрыты навсегда. При редизайне блок убирают через display: none «на время», а файл остаётся в разметке и грузится у всех. Так у нас нашёлся декоративный PNG с дымом на 94 КБ, который не показывался ни на одной ширине экрана.
Лечение
- Мёртвое — удалить. Если картинка не показывается ни при каком состоянии страницы, её место не в CSS, а в корзине.
- Скрытое до действия — отложить адрес. Вместо
srcиsrcsetположить адреса вdata-srcиdata-srcsetи подставить их, когда элемент раскрывают. - Порядок подстановки: сначала
<source>внутри<picture>, потом<img>. Иначе браузер успеет начать загрузку запасного формата изimgдо того, как узнает про avif и webp.
// при раскрытии меню / вкладки / фильтра — подставить отложенные адреса
function revealImages(container) {
container.querySelectorAll("source[data-srcset]").forEach(s => {
s.srcset = s.dataset.srcset; // сначала <source>…
s.removeAttribute("data-srcset");
});
container.querySelectorAll("img[data-src]").forEach(img => {
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
img.src = img.dataset.src; // …потом <img>
img.removeAttribute("data-src");
});
}
menuButton.addEventListener("click", () => revealImages(mobileMenu), { once: true });
Для вкладок и слайдеров удобно подставлять адреса ещё и заранее — при наведении на вкладку или переключении слайда, — чтобы картинка была готова к моменту показа.
Как найти у себя
В консоли браузера — какие картинки загружены, но находятся внутри невидимых элементов:
[...document.images]
.filter(i => i.complete && i.naturalWidth && !i.offsetParent)
.map(i => i.currentSrc.split("/").pop())
offsetParent равен null у элементов внутри display: none. Всё, что попало в список, загружено зря — по крайней мере до первого действия посетителя.
Подозреваете лишние картинки в окне отрисовки?
Пришлите адрес — найду скрытые и мёртвые картинки на ключевых страницах.
// загружаю комментарии…