Многие картинки нельзя «просто сделать ленивыми»: их выводит плагин миниатюр, слайдер или конструктор блоков — у каждого свой формат разметки, и править каждый шаблон долго и рискованно. Нужен способ отложить загрузку в уже готовой разметке, не трогая того, кто её сгенерировал.

Это двадцать четвёртая часть цикла «PageSpeed без мифов».

Идея

  1. На сервере маленькая функция берёт готовый HTML картинки и переносит адреса в data-атрибуты: src → data-src, srcset → data-srcset, фон — в data-bg. Браузеру нечего качать.
  2. В браузере после ворот «после первой отрисовки» (восьмая часть) скрипт возвращает адреса на место. Для слайдов — ещё и при переключении.

Разметка при этом одна для всех посетителей — значит, кэш страниц не ломается, и ничего не прячется от проверки: любой браузер получает ту же страницу и загружает картинки после отрисовки.

Код

/**
 * Отложить загрузку картинок в готовой разметке: src/srcset → data-*, фон → data-bg.
 * Работает с выводом любой функции (плагина миниатюр, слайдера, шаблона) — не нужно
 * переписывать каждый шаблон под свой формат.
 */
function defer_picture(string $html): string
{
	// <source srcset> и <img src/srcset> — адреса в data-атрибуты (каждый тег целиком)
	$html = preg_replace_callback('~<(?:source|img)\b[^>]*>~i',
		fn($m) => preg_replace('~\s(srcset|src)=~i', ' data-$1=', $m[0]), $html);
	// прозрачная заглушка, чтобы <img> оставался валидным и держал размеры
	$html = preg_replace('~<img\b(?![^>]*\ssrc=)~i', '<img src="data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="', $html);
	// style="background-image:url(...)" → data-bg
	return preg_replace('~style="background-image:\s*url\(([^)]+)\);?"~i', 'data-bg="$1"', $html);
}

echo defer_picture(render_slide_picture($slide));
import afterFirstPaint from "./afterFirstPaint";

export function reveal(root = document) {
  root.querySelectorAll("[data-srcset]").forEach(el => { el.srcset = el.dataset.srcset; el.removeAttribute("data-srcset"); });
  root.querySelectorAll("img[data-src]").forEach(el => { el.src = el.dataset.src; el.removeAttribute("data-src"); });
  root.querySelectorAll("[data-bg]").forEach(el => { el.style.backgroundImage = `url(${el.dataset.bg})`; el.removeAttribute("data-bg"); });
}

afterFirstPaint().then(() => reveal());          // всё отложенное — после ворот
slider.on("slideChange", s => reveal(s.slides[s.activeIndex])); // и при переключении слайда

Не откладывайте картинку первого экрана. Функция — для того, что ниже сгиба, в соседних слайдах и скрытых блоках. Картинке, которая станет LCP, нужно обратное: загрузка сразу и высокий приоритет (двадцать третья часть).

Результаты

Приём мы применили на нескольких сайтах, где вес окна первой отрисовки сидел именно в картинках ниже сгиба: фоновые иллюстрации блоков, слайдеры, постеры видео, декор.

СтраницаОкно первой отрисовки: доПосле
«О компании»6885 КБ274 КБ
страница дилеров2546 КБ387 КБ
товарная783 КБ453 КБ
раздел статей4876 КБ1161 КБ
разные сайты, рабочие версии

На том же сайте, где товарная похудела с 783 до 453 КБ, оценка товарной выросла с 88 до 93.

Что осталось и почему

В разделе статей после правки в окне ещё было 1161 КБ, и 804 из них — постеры видео: файлы 1100×619 для плиток размером 361×203. Это уже другая проблема — размер файла не под место показа. Отложенная загрузка её не решает, нужен правильный размер миниатюры. О постерах и размерах — в следующих частях.

А если скрипты не загрузились?

Картинки ниже сгиба не покажутся — заглушки останутся. Для страхования можно добавить <noscript> с обычной разметкой. Но если у сайта не работают скрипты, проблемы у него будут и без этого.

Картинки ниже сгиба тянут оценку вниз?

Пришлите адрес — найду, какие картинки стоит отложить, и посчитаю эффект.

· полезно?
АЧ
Алексей Чикун веб-разработчик, команда TRIOHEAD

Делаю сайты и сервисы целиком: от вёрстки до сервера. Пишу о том, что проверил на своих проектах.

Комментарии

// загружаю комментарии…

Комментарии публикуются после проверки. Ссылки и HTML не принимаются.