Жалоба звучала так: «на айфоне обновляю главную где-нибудь в середине, и она маленькими рывками ползёт вниз». Мы прогнали замер сдвигов вёрстки (CLS) в Chrome с медленной сетью и замедленным процессором — ноль во всех прогонах. А страница при этом действительно ползла.
Это первая часть цикла «Вёрстка без сюрпризов» — о багах, которые видит только посетитель.
Почему Chrome показывает ноль
У Chrome есть привязка прокрутки (scroll anchoring). Если содержимое выше видимой области меняет высоту, браузер сдвигает прокрутку так, чтобы то, на что смотрит человек, осталось на месте. Сдвига на экране нет — и в метрику сдвигов он не записывается.
У Safari привязки прокрутки нет. Блок выше экрана вырос на 30 пикселей — всё, что видит человек, уехало на 30 пикселей вниз. Поэтому жалоба приходит только с айфонов, а все замеры CLS показывают ноль.
CLS в Chrome равен нулю — не значит, что ничего не двигается. Это значит, что Chrome компенсировал сдвиг сам.
Что мерить вместо CLS
- высоту документа во времени —
document.documentElement.scrollHeightкаждые 100–150 мс от загрузки до тишины; - абсолютное положение конкретного элемента до и после события:
getBoundingClientRect().top + window.scrollY; - в дробных пикселях —
toFixed(2). Разница в 0,14 пикселя без дробей не видна вовсе, а на устройстве она превращается в 2 пикселя.
const log = [];
const el = document.querySelector('.productLine h2');
const t0 = performance.now();
const timer = setInterval(() => {
log.push({
t: Math.round(performance.now() - t0),
doc: document.documentElement.scrollHeight,
y: (el.getBoundingClientRect().top + scrollY).toFixed(2),
});
}, 120);
setTimeout(() => { clearInterval(timer); console.table(log); }, 8000);
Опрос — setInterval, а не цикл на каждый кадр. Чтение геометрии на каждом кадре заставляет браузер пересчитывать раскладку, и на тяжёлой странице сам замер начинает тормозить её на секунды.
Что нашли этим замером
Три причины, все три — сдвиги выше экрана:
- подмена шрифта — 142 пикселя на страницу. Пока грузится веб-шрифт, текст рисуется системным, а он уже: часть названий помещалась в две строки вместо трёх. Лечение — запасной шрифт с подогнанными метриками, подробно в 19-й части цикла про PageSpeed;
- заглушки ajax-блоков меньше самих блоков — 272 пикселя;
aspect-ratioи округление ширины слайдера — 2 пикселя на каждой из двадцати строк. Об этом завтра.
Заглушки: одна на всех — значит неверная у всех
Блоки, которые грузятся по мере прокрутки, сначала представлены заглушкой фиксированной высоты. У нас она была общей — 590 пикселей. Реальные высоты блоков на телефоне:
| Ширина экрана | 360 | 390 | 430 | 560 |
|---|---|---|---|---|
| блок дилеров | 708 | 767 | 846 | 1102 |
| подвал | 640 | 685 | 746 | 945 |
| резерв | 590 | 590 | 590 | 590 |
Когда блок загружался, всё ниже сдвигалось на сотню-полторы пикселей, а в Safari — и то, на что смотрел человек.
Важная деталь: содержимое этих блоков свёрстано в vw, поэтому их высота строго пропорциональна ширине. 708 / 360 = 767 / 390 = 846 / 430 = 1102 / 560 — ровно 196,7vw. Значит, и резерв надо задавать в vw и для каждого блока свой:
.asyncBlock[data-block="dealers"] { min-height: 196.7vw; }
@media (min-width: 821px) {
.asyncBlock[data-block="dealers"] { min-height: 837px; }
}
Проверка: прогнать реальные высоты блока по 6–8 ширинам и сверить с резервом. Если высота пропорциональна ширине — резерв в vw, если нет — несколько ступеней по ширинам.
Что проверили и что не подтвердилось
- «пересчёт слайдера при схлопывании адресной строки айфона» — замер изменения только высоты окна дал ноль;
- кадры галерей без размеров — у них есть
widthиheight, загрузка не меняет рамку; - сокращение DOM и
content-visibilityк этой задаче отношения не имеют.
Страница «дёргается» на айфоне, а в Chrome всё ровно?
Пришлите адрес — найду, что меняет высоту выше экрана, замером, а не на глаз.
// загружаю комментарии…