Многие картинки нельзя «просто сделать ленивыми»: их выводит плагин миниатюр, слайдер или конструктор блоков — у каждого свой формат разметки, и править каждый шаблон долго и рискованно. Нужен способ отложить загрузку в уже готовой разметке, не трогая того, кто её сгенерировал.
Это двадцать четвёртая часть цикла «PageSpeed без мифов».
Идея
- На сервере маленькая функция берёт готовый HTML картинки и переносит адреса в data-атрибуты:
src→data-src,srcset→data-srcset, фон — вdata-bg. Браузеру нечего качать. - В браузере после ворот «после первой отрисовки» (восьмая часть) скрипт возвращает адреса на место. Для слайдов — ещё и при переключении.
Разметка при этом одна для всех посетителей — значит, кэш страниц не ломается, и ничего не прячется от проверки: любой браузер получает ту же страницу и загружает картинки после отрисовки.
Код
/**
* Отложить загрузку картинок в готовой разметке: 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> с обычной разметкой. Но если у сайта не работают скрипты, проблемы у него будут и без этого.
Картинки ниже сгиба тянут оценку вниз?
Пришлите адрес — найду, какие картинки стоит отложить, и посчитаю эффект.
// загружаю комментарии…