Чисті CSS-скелетні екрани: ефект блиску, зменшена анімація та продуктивність

Було корисно?

Ваш API працює. Ваша база даних працює. Ваш CDN працює. І все одно користувачі скаржаться, що сторінка «відчувається повільною».
Причина в тому, що сприйнятна продуктивність — це функція продукту, а ваші порожні білі прямокутники по суті є повідомленням про помилку завантаження з кращими манерами.

Скелетні екрани — особливо ті, що реалізовані лише на CSS — можуть зробити продукт відчутно швидким без обману. Але якщо зробити їх погано, вони
розряджають батарею, пригальмовують на слабких пристроях, зачіпають чутливість до руху й тихо руйнують бюджет рендерингу.
Це практичний посібник для продакшну: як їх будувати, як тримати швидкими та як діагностувати, коли щось іде не так.

Що таке скелетні екрани (і чим вони не є)

Скелетний екран — це заповнювач інтерфейсу, що нагадує остаточне розміщення елементів. Не остаточний контент — лише структура.
Мета — зменшити сприйняту затримку й запобігти ефекту «порожня сторінка → раптовий стрибок». Він дає вам час, поки
мережа, JS, зображення й рендеринг роблять свою справу.

Скелети не замінюють продуктивність. Це договір з користувачем: «Ми працюємо, ось де елементи з’являться».
Якщо ваш додаток регулярно чекає дві секунди на JSON, який можна кешувати, ваш скелет — по суті декоративне вибачення.

Найкращі скелетні екрани добре виконують три завдання:

  • Стабільність: вони резервують простір, щоб сторінка не стрибала (CLS залишається низьким).
  • Правдоподібність: вони достатньо точно відповідають кінцевому макету, щоб виглядати намірено.
  • Ефективність: вони не коштують анімаційно дорожче, ніж прихований ними контент.

Чисті CSS-скелети привабливі, бо зменшують навантаження JS, швидко відвантажуються й деградують плавно.
Але «тільки CSS» не означає автоматично «швидко». CSS має свої власні дорогі сторони.

Цікаві факти та трохи історії

  • Факт 1: Скелетні екрани стали масовими в середині 2010-х, коли мобільні додатки популяризували «завантаження з контекстом» замість спінерів.
  • Факт 2: Класичний ефект блиску — це по суті рухомий підсвіт над базовим кольором — схожий на трюки «спекулярного проходу», що використовувалися в ранніх UI-гланцевих ефектах.
  • Факт 3: Сучасні браузери рендерять сторінки через конвеєр (style → layout → paint → composite). Анімація скелета може навантажувати paint або composite залежно від того, що ви анімуєте.
  • Факт 4: Анімація transform і opacity зазвичай дешевша, бо часто залишається на композиторі, пропускаючи репейнти.
  • Факт 5: Анімація позицій градієнта часто тригерить репейнти; градієнти не «безкоштовні», і великий мерехтливий градієнт може стати щомиттєвим податком.
  • Факт 6: prefers-reduced-motion з’явився на основних платформах як частина загального поштовху доступності — чутливість до руху — реальна проблема, а не просто перемикач для забави.
  • Факт 7: Скелетні екрани можуть ефективніше зменшити сприйнятну затримку, ніж спінери, бо показують прогрес у «формі», навіть якщо нічого фактично не рухається вперед.
  • Факт 8: Cumulative Layout Shift (CLS) увійшов у мейнстрім з Core Web Vitals, змінивши уявлення про «хороший UX при завантаженні» для SEO та утримання користувачів.
  • Факт 9: Апаратне прискорення GPU не є чарівною паличкою; примусова промоція шарів скрізь може збільшити використання пам’яті й викликати ще гірші пригальмовування, коли GPU закінчується місце.

Один цитат вартий того, щоб приклеїти на стикер:
Надія — не стратегія. — Джин Кранц.
Скелетні екрани — це надія з CSS. Тримайте їх чесними й вимірюваними.

Надійна база: чисті CSS-блоки скелета

Почніть з нудної версії. Потрібна база, що виглядає прийнятно, навіть якщо анімація вимкнена,
пристрій повільний або користувач увімкнув зменшену анімацію. Анімація — це покращення, а не основа.

Мінімальний компонент скелета (ще без блиску)

Основна ідея: використовуйте нейтральний фон, заокруглені кути й розміри-заповнювачі, що відповідають реальному контенту.
Уникайте надмірних деталей. Скелет, який намагається імітувати кожну типографічну тонкість — це ще один UI, за яким потрібно стежити.

cr0x@server:~$ cat skeleton.css
:root {
  --sk-bg: #e9ecef;
  --sk-fg: #f8f9fa;
  --sk-radius: 10px;
}

.skeleton {
  background: var(--sk-bg);
  border-radius: var(--sk-radius);
  position: relative;
  overflow: hidden;
}

.skeleton.line { height: 1em; }
.skeleton.line.sm { height: 0.8em; }
.skeleton.line.lg { height: 1.2em; }

.skeleton.avatar {
  width: 48px;
  height: 48px;
  border-radius: 999px;
}

.skeleton.block {
  height: 160px;
}

.skeleton + .skeleton { margin-top: 12px; }

Це дає заповнювачі, що не рухаються, не мерехтять і не спалюють CPU. Це також означає, що ваша історія зі зменшеною анімацією вже прийнятна.

Резервуйте простір макету, щоб уникнути CLS

CLS — це не лише проблема зображень. Це також «скелет не відповідає реальному макету» і «шрифти рендеряться пізно».
Скелети повинні резервувати той самий простір, що і завантажений контент: ті ж висоти, ті ж відступи, ті ж колонки сітки.
Вимірюйте реальний UI і віддзеркалюйте його.

Якщо ви не знаєте точних висот, використовуйте обмеження: співвідношення сторін для медіа, min-height для карток і стеки ліній для тексту.
І не ховайте скелети за допомогою display: none прямо перед появою контенту, якщо це викликає масовий перелайоути.

Ефект блиску: як це працює і як робити без нагріву лептопів

Ефект блиску — це рухомий підсвіт, який каже «завантажується». Це також найпростіший спосіб випадково анімувати градієнт, що перерисовуєсь
по 40 заповнювачах і дивуватися, чому прокручування відчувається як тягнути диван.

Підхід A (поширений): анімація градієнта фону

Це класичний фрагмент, який ви бачили скрізь: лінійний градієнт, що зміщується по елементу.
Це може бути прийнятно для невеликих поверхонь, але часто репейнтить кожен кадр, особливо якщо зона блиску велика.

cr0x@server:~$ cat shimmer-gradient.css
.skeleton.shimmer {
  background: linear-gradient(90deg, var(--sk-bg) 25%, var(--sk-fg) 37%, var(--sk-bg) 63%);
  background-size: 400% 100%;
  animation: sk-shimmer 1.2s ease-in-out infinite;
}

@keyframes sk-shimmer {
  0%   { background-position: 100% 0; }
  100% { background-position: 0 0; }
}

Ризик: великі репейнти. Градієнти — це не просто колір; це відмалювана картинка. Коли ви анімуєте позицію, браузер часто робить репейнт.
Іноді його можна оптимізувати; іноді — ні. Не ставте продуктивність прокрутки на «іноді».

Підхід B (рекомендовано): анімуйте накладку псевдо-елемента через transform

Ось більш дружній до продакшну підхід: скелет має плоский фон. Псевдо-елемент малює смугу підсвітки.
Ви анімуєте псевдо-елемент за допомогою transform. Це дає композитору шанс виконати роботу без репейнтів усього елемента.

cr0x@server:~$ cat shimmer-transform.css
.skeleton.shimmer {
  background: var(--sk-bg);
}

.skeleton.shimmer::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    rgba(255,255,255,0) 0%,
    rgba(255,255,255,0.55) 50%,
    rgba(255,255,255,0) 100%
  );
  animation: sk-sweep 1.3s ease-in-out infinite;
  will-change: transform;
}

@keyframes sk-sweep {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

Це не гарантує уникнення репейнтів всюди (бравзери відрізняються), але зазвичай зменшує площу ураження.
Підсвітка стає кандидатом у окремий шар. Базовий фон залишається стабільним.

Перший жарт (ми дозволяємо собі рівно два): Скелетні лоудери як запрошення на зустріч — якщо вони тривають занадто довго, всі думають, що щось зламалось.

Тримайте блиск тонким і короткочасним

Блиск — не неонова вивіска. Використовуйте низьку контрастність і тривалість близько 1.1–1.6 секунд. Швидше виглядає нервово. Повільніше — ніби застряло.
І коли контент з’являється, негайно зупиніть анімацію. Не залишайте блиск під завантаженим контентом, бо забули видалити клас.

Не анімуйте все підряд

Якщо у вас список із 30 рядків, анімувати кожен ряд — перебір. Анімуйте перші кілька рядків або один представницький блок.
Користувачу не потрібно 30 синхронних танцюючих градієнтів, щоб зрозуміти «завантажується». Ваш CPU точно не потребує цього.

Затримки (staggering): гарно виглядає, але небезпечно для продуктивності

Дизайнери люблять затримки анімацій. SRE люблять графіки без дивних піків. Затримки можуть виглядати чудово,
але також можуть перешкоджати оптимізаціям браузера, бо кожен елемент в різній фазі анімації.
Якщо у вас багато скелетів, віддавайте перевагу одній спільній часовій шкалі анімації. Якщо мусите ставити затримки, робіть це помірно й з обмеженням числа.

Зменшена анімація: поважати користувачів, не випускаючи «мертвий» UI

Зменшена анімація — це не опціонально. Деякі користувачі мають нудоту або мігрені від постійного руху. Інші користувачі на режимі збереження енергії просто хочуть, щоб сторінка не сіпалась.
Ваше завдання — забезпечити еквівалентний досвід: зрозуміло, що завантаження триває, але без анімованих ефектів.

Використовуйте prefers-reduced-motion, щоб вимкнути блиск

cr0x@server:~$ cat reduced-motion.css
@media (prefers-reduced-motion: reduce) {
  .skeleton.shimmer::after {
    animation: none;
    opacity: 0.0;
  }
  .skeleton.shimmer {
    background: var(--sk-bg);
  }
}

Це залишає заповнювач видимим, але прибирає рухому підсвітку. Користувачі бачать структуру й резервований простір.
Вони просто не отримують анімований сигнал. Це нормально.

Розгляньте немотичний сигнал

Якщо ви хочете надати натяк «живості» без руху, можна зробити повільне, низькоконтрастне мерехтіння на opacity.
Але якщо користувач обрав зменшену анімацію, «пульсація» для багатьох теж вважається рухом. Вимикайте її в режимі reduce за замовчуванням.

Деталі доступності, які часто забувають

  • Екранні читалки: скелети не повинні читатися як реальний контент. Використовуйте aria-hidden="true" для суто декоративних блоків скелета.
  • Фокус: не розміщуйте фокусовані елементи всередині контейнерів скелета. Порядок табуляції не має вести користувача через заповнювачі.
  • Контраст кольору: скелети не повинні виглядати як вимкнений реальний текст. Вони — заповнювачі, а не «заблокований» контент.

Модель продуктивності: paint, composite і чому градієнти дорогі

Якщо ви хочете скелети, що не підлагують, потрібно мати базову модель рендерингу в голові.
Не академічну версію. Ту, що відповідає на питання «що ламається о 9:42 під час піку трафіку».

Що насправді робить браузер

  1. Style: обчислює CSS-правила.
  2. Layout: обчислює розміри й позиції.
  3. Paint: малює пікселі в шари.
  4. Composite: переміщує шари й зливає їх у фінальний кадр.

Скелети шкодять вам, коли вони викликають:

  • Перелайаути: скелет постійно змінює розмір або вмикається/вимикається, що тригерить релайаут.
  • Дорогий paint: великі градієнти, фільтри розмиття, box-shadow або маски, які треба перемальовувати кожен кадр.
  • Вибух шарів: занадто багато промотованих шарів (через will-change або анімацію), що створює тиск на пам’ять і гіршу продуктивність.

Практичні правила, що виживають у продакшні

  • Анімуйте transform, а не background-position, коли можливо. Transform часто залишається на композиторі.
  • Тримайте поверхні блиску маленькими. Блиск на весь в’юпорт — запрошення до пропущених кадрів.
  • Обережно використовуйте content-visibility. Вона може пришвидшити рендеринг поза вьюпортом, але може створити «поп-ін» сюрпризи, якщо ви не задали розміри заповнювачів.
  • Не спамте will-change. Застосовуйте його тільки до елементів, які анімуєте, і тільки на час анімації.

Коли скелети — не той інструмент

Якщо ваш контент дуже мінливий, скелети можуть вводити в оману. Приклад: стрічка з картками, де може бути 2 рядки або 20 рядків.
У такому випадку використайте простіший заповнювач (оден блок на картку) або реальні резерви простору з min-height.

І якщо ваш час завантаження в основному витрачається на декодування зображень або підстановку шрифтів, скелети не виправлять фундаментальну затримку.
Вони просто стоятимуть і ввічливо мерехтітимуть, поки пристрій бореться.

Швидкий план діагностики

Коли анімації скелета «ведуть себе дивно», ви можете втратити години, копаючись не в тій ділянці. Це порядок дій, який найчастіше швидко знаходить винуватця.

По-перше: підтвердіть тип уповільнення

  • CPU-завантаження? Вентилятор пришвидшується, DevTools показує довгі «Recalculate Style»/«Paint».
  • GPU-обмеження? Частота кадрів падає під час анімації, особливо на екранах з високим DPI або коли багато шарів.
  • Основний потік заблоковано JS? Скелет сіпається, коли вмикаються аналітика або гідрація.
  • Мережа? Тривалість скелета велика; анімація плавна, але триває секунди.

По-друге: локалізуйте проблему

  • Вимкніть клас анімації скелета й перезавантажте: зникає джанк?
  • Зменшіть кількість елементів скелета (наприклад, 30 → 5): чи масштаб лінійний або падає катастрофічно?
  • Перемкніть реалізацію блиску (background-position vs pseudo-element transform): чи зменшився час paint?

По-третє: валідуйте за допомогою інструментів, а не відчуттів

  • Використовуйте профайлер браузера, щоб знайти гарячі місця layout/paint/composite.
  • Використовуйте метрики на рівні ОС для виявлення тротлінгу CPU/GPU або термічних обмежень.
  • Перевірте регресії CLS: скелет повинен стабілізувати макет, а не дестабілізувати його.

Практичні завдання з командами: виміряти, перевірити, вирішити

Це практичні завдання, які можна виконати на девбоксі або тестовому хості для діагностики проблем продуктивності скелетів.
Кожне завдання містить команду, що означає її вивід і яке рішення з цього випливає.
Без надгероїв. Тільки докази.

Завдання 1: Перевірити поведінку зменшеної анімації на рівні ОС (GNOME)

cr0x@server:~$ gsettings get org.gnome.desktop.interface enable-animations
true

Що це означає: true означає, що системні анімації дозволені; false зазвичай корелює з налаштуванням зменшеної анімації.

Рішення: Якщо користувачі скаржаться на рух, відтворіть сценарій з вимкненими анімаціями і переконайтеся, що ваш CSS-путь з prefers-reduced-motion працює як треба.

Завдання 2: Перевірити навантаження CPU під час анімації скелета (Linux)

cr0x@server:~$ pidstat -dur 1 5
Linux 6.8.0 (server)  12/29/2025  _x86_64_  (8 CPU)

#      Time   UID       PID  %usr %system  %guest  %CPU   CPU  kB_rd/s  kB_wr/s  kB_ccwr/s iodelay  Command
12:10:01  1000     24138  38.00    4.00    0.00 42.00     3      0.00     12.00      0.00       0  chrome

Що це означає: Chrome споживає ≈42% CPU у вікні вимірювання.

Рішення: Якщо піки CPU корелюють з блиском, перейдіть на shimmer через transform, зменшіть кількість скелетів або припиніть анімацію поза вьюпортом.

Завдання 3: Виявити, чи є тротлінг GPU (Linux + Intel/AMD)

cr0x@server:~$ sudo intel_gpu_top -s 1000
intel_gpu_top -  Intel(R) Graphics -  Frequency 600MHz -  0.00/  0.00 Watts
      IMC reads:   4121 MiB/s  writes:  623 MiB/s
        Render/3D:  78.21%  Blitter:   0.00%  Video:  0.00%

Що це означає: Рендер/3D зайнятий (~78%). Анімація може змушувати важке композитування або великі текстурні шари.

Рішення: Зменшіть кількість шарів (не застосовуйте will-change скрізь), зменшіть площу блиску і уникайте повноширових градієнтів на багатьох елементах.

Завдання 4: Підтвердити, чи Chrome використовує апаратне прискорення

cr0x@server:~$ google-chrome --version
Google Chrome 121.0.6167.160

Що це означає: У вас є відома збірка Chrome; тепер ви можете відтворювати помилку послідовно на різних машинах.

Рішення: Прив’яжіть кроки відтворення до версії браузера. Регресії анімацій можуть бути специфічні для версії браузера; ставтесь до цього як до залежності.

Завдання 5: Зняти швидкий headless trace за допомогою Playwright

cr0x@server:~$ node -e "const { chromium } = require('playwright'); (async () => { const b = await chromium.launch(); const p = await b.newPage(); await p.tracing.start({ screenshots: false, snapshots: true }); await p.goto('http://localhost:8080'); await p.waitForTimeout(4000); await p.tracing.stop({ path: 'trace.zip' }); await b.close(); })();"

Що це означає: Ви отримали trace.zip, який можна проінспектувати для перегляду layout/paint активності в часі.

Рішення: Якщо ви бачите часті події paint під час блиску, надавайте перевагу sweep на основі transform або зменшіть уражену область.

Завдання 6: Перевірити, чи пристрій термічно тротлиться (Linux)

cr0x@server:~$ sensors
coretemp-isa-0000
Adapter: ISA adapter
Package id 0:  +92.0°C  (high = +100.0°C, crit = +100.0°C)
Core 0:        +90.0°C  (high = +100.0°C, crit = +100.0°C)

Що це означає: Пристрій гарячий. Термічний тротлінг може спричиняти сіпання анімацій, навіть якщо ваш CSS «в порядку».

Рішення: Тестуйте на холодному й гарячому пристрої. Якщо блиск сіпається тільки коли пристрій гарячий, потрібно знизити вартість анімації й припиняти анімацію поза вьюпортом.

Завдання 7: Перевірити падіння кадрів через простий лічильник FPS (без прапорців Chrome)

cr0x@server:~$ node -e "console.log('Open DevTools -> Rendering -> enable FPS meter. Watch for drops during skeleton shimmer.');"
Open DevTools -> Rendering -> enable FPS meter. Watch for drops during skeleton shimmer.

Що це означає: Ви використовуєте вбудований FPS-метр, щоб бачити, чи тримаєте 60fps/120fps або падаєте.

Рішення: Якщо FPS падає під час блиску, але не під час статичних скелетів, анімація — причина. Змініть стратегію анімації або область її застосування.

Завдання 8: Аудит зсувів макета за допомогою Lighthouse CLI (локально)

cr0x@server:~$ lighthouse http://localhost:8080 --only-categories=performance --output=json --quiet --chrome-flags="--headless" | jq '.audits["cumulative-layout-shift"].numericValue'
0.19

Що це означає: CLS = 0.19 — не дуже добре. Можливо, скелети не відповідають остаточному макету, або зображення/шрифти зміщують вміст.

Рішення: Виправте резерв простору: явні висоти/співвідношення сторін, співпадаючі розміри скелета і фінального UI, і контролюйте поведінку підвантаження шрифтів.

Завдання 9: Підтвердити вплив підстановки шрифтів на зсув

cr0x@server:~$ rg -n "font-display" -S .
assets/css/fonts.css:12:  font-display: swap;

Що це означає: Шрифти використовують swap. Це може спричиняти зсув, якщо метрики fallback-шрифту суттєво відрізняються.

Рішення: Якщо CLS високий, розгляньте сумісні за метриками fallback-стеки і переконайтеся, що висоти рядків скелета відповідають фінальному відрендереному тексту.

Завдання 10: Виявити, чи ви відсилаєте надлишковий CSS для скелетів

cr0x@server:~$ gzip -c dist/app.css | wc -c
48219

Що це означає: Стиснутий CSS ≈48KB. Якщо стилі скелета — велика частина, це пропускна здатність і час парсингу, які ви платите на кожному маршруті.

Рішення: Розділіть критичний CSS. Тримайте стилі скелетів маленькими, перевикористовуваними і уникайте генерації сотень унікальних класів для кожної форми заповнювача.

Завдання 11: Перевірити випадкові нескінченні анімації на завантаженому контенті

cr0x@server:~$ rg -n "shimmer|skeleton" dist/app.js dist/app.css
dist/app.css:44:.skeleton.shimmer::after { animation: sk-sweep 1.3s ease-in-out infinite; will-change: transform; }
dist/app.js:221:document.body.classList.add("loading")

Що це означає: Ви досі додаєте глобальний клас loading. Якщо його не видаляти надійно, анімація може продовжуватись.

Рішення: Додайте жорсткий таймаут і прибирання стану. Також обмежте блиск тільки реальними елементами скелета, а не всією сторінкою.

Завдання 12: Переконатися, що ви не створюєте тисячі шарів через will-change

cr0x@server:~$ rg -n "will-change" -S dist/app.css
44:  will-change: transform;
101: will-change: transform;
102: will-change: opacity;
103: will-change: transform, opacity;

Що це означає: Кілька використань will-change. Якщо його застосовано широко (наприклад, на кожному елементі списку), це може створити тиск на пам’ять.

Рішення: Обмежте will-change вузьким набором елементів і приберіть його, коли анімація зупиняється. Не «оптимізуйте» постійною просуваючою промоцією шарів.

Завдання 13: Переконатися, що елементи скелета перестають анімуваися поза вьюпортом (базова перевірка)

cr0x@server:~$ node -e "console.log('If you have a long list, scroll: does CPU stay high? If yes, consider pausing animation offscreen via IntersectionObserver toggling a class.');"
If you have a long list, scroll: does CPU stay high? If yes, consider pausing animation offscreen via IntersectionObserver toggling a class.

Що це означає: Це поведінковий тест: CPU має падати, коли анімовані елементи виходять за межі вьюпорту.

Рішення: Якщо CPU лишається високим, призупиніть блиск поза вьюпортом. Чистий CSS не може надійно визначити видимість, тому потрібен мінімальний JS для перемикання класів.

Завдання 14: Перевірити, чи блиск спричиняє шторми репейнтів (інструменти X11)

cr0x@server:~$ xrestop -b | head
res-base Wins  pixmap   Other   Total  Pid  Name
  623K     35   1816K   4102K   6541K 24138 chrome

Що це означає: Якщо пам’ять pixmap швидко зростає під час блиску, ви можливо генеруєте великі поверхні/шари.

Рішення: Зменшіть площу блиску, уникайте великих градієнтів і скоротіть кількість одночасно анімованих скелетів.

Другий жарт (на цьому все): Додавати will-change скрізь — як маркувати кожну коробку «КРУПНЕ» — це не зробить доставку швидшою, просто дратує всіх.

Три корпоративні міні-історії з реального життя

Міні-історія 1: Інцидент, спричинений хибним припущенням

Одна команда продукту випустила «новий досвід завантаження» для дашборда. Він виглядав круто: картки-скелети з блиском, затримками та легким розмиттям.
Припущення було просте: CSS-анімація дешевша за JS-спінери, отже це безпечно. Ніхто не профілював на слабкому залозі, бо у стейджингу були тільки потужні ноутбуки.

У день релізу почали надходити тікети підтримки з описом «заморожування прокрутки» та «витрати батареї». Метрики були дивні: затримки бекенду стабільні.
Хітрейт CDN нормальний. Але сесії користувачів на певних пристроях мали меншу тривалість і більше rage-click.
Основний потік не був заблокований JS; він тону в роботі paint.

Блиск реалізовували через анімовані градієнти фону на кожній картці — десятки одночасно — плюс фільтр розмиття, щоб «пом’якшити» підсвітку.
Браузер перемальовував великі області кожен кадр. На слабших GPU композитинг іноді падав у неприємні режими.
Результат — той самий джанк, що відчувається зубами.

Виправлення було майже ганебним: прибрати розмиття, перестати анімувати позиції градієнтів і анімувати псевдо-елемент transform лише на кількох видимих картках.
Також вимкнули блиск при зменшеній анімації й у режимі низького енергоспоживання.
UI став трохи менш ефектним, і продукт перестав «варити» телефони.

Міні-історія 2: Оптимізація, що відкотилась

Інша команда хотіла «вирішити» продуктивність блиску, форсувавши GPU-прискорення. Вони додали will-change: transform на кожен елемент скелета,
плюс ще кілька компонент «на всякий випадок». У локальних тестах початковий FPS покращився. Команда відсвяткувала і змінила код.

Через тиждень вони почали бачити періодичні сіпання — гірші, ніж раніше — на довгих сторінках.
Користувачі повідомляли, що при перемиканні вкладок і поверненні сторінка повільно перерисовувалась.
На деяких пристроях браузер навіть іноді показував чорні прямокутники під час прокрутки. Не часто. Але достатньо, щоб зруйнувати довіру.

Проблема не в тому, що will-change зло. Проблема в масштабі. Промоція занадто великої кількості елементів у власні шари збільшує використання пам’яті й накладні витрати на управління.
Коли система закінчилася по пам’яті GPU, композитору доводилось жонглювати поверхнями.
«Оптимізація» перетворилась на шаровий треш.

Відкат був хірургічний: застосовувати will-change тільки до псевдо-елемента підсвітки, тільки під час анімації й тільки для скелетів над згином сторінки.
Також вони зменшили кількість скелетів на довгих списках, використовуючи менше заповнювачів до скролу користувача.
Середня продуктивність знову стала «нудною», що є найвищою похвалою для продакшн UI.

Міні-історія 3: Нудна але правильна практика, що врятувала ситуацію

Додаток для платежів мав випустити редизайн під суворими вимогами надійності: будь-який глитч під час чекауту вважався критичним.
Команда наполягала на підході «чеклисти перш за все». Не гламурно. Ефективно.

Вони визначили контракти скелетів по компонентам: точні висоти, відступи і зарезервовані співвідношення сторін для зображень.
Написали невеликий набір візуальних регресій, що робив скриншоти перед/після переходів завантаження.
Також додали автоматичну перевірку Lighthouse, яка фейлила CI, якщо CLS перевищував поріг на ключових флоу.

Під час одного релізу, непомітна на перший погляд типографічна правка змінила line-height і спричинила, що реальний контент став вищим за скелет.
Візуальний набір відловив це відразу. Регресія CLS ніколи не потрапила в продакшн.
Команда відкоригувала стеки ліній скелета і оновила fallback-шрифти, сумісні за метриками.

Нічого драматичного не сталося. Ніяких інцидентів. Просто тиха безперервність.
Найкраща робота з надійності часто виглядає як не-подія, бо вона запобігла події ще до її виникнення.

Типові помилки: симптом → корінь → виправлення

1) Симптом: прокрутка сіпається тільки доки видимі скелети

Корінь: блиск реалізовано через анімовані фон-градієнти на багатьох великих елементах, що спричиняє часті репейнти.

Виправлення: перейдіть на sweep псевдо-елемента, анімацію через transform; зменшіть кількість анімованих скелетів; уникайте розмиття та важких тіней.

2) Симптом: скелет виглядає нормально, але CLS все одно високий

Корінь: розміри скелета не відповідають фінальному макету (різні метрики шрифтів, відсутнє співвідношення сторін для зображень, змінні висоти контенту).

Виправлення: резервуйте точний простір за допомогою фіксованих висот, блоків з aspect-ratio і стосів ліній; переконайтеся, що скелет і фінальний компонент ділять правила макета.

3) Симптом: анімація продовжується після завантаження контенту

Корінь: клас loading не видаляється надійно (зміна маршрутів, помилкові шляхи, abort-запити), або блиск застосовано глобально.

Виправлення: прив’язуйте видимість скелета до стану; додайте прибирання при успіху, помилці та abort; обмежте блиск лише елементами скелета.

4) Симптом: користувачі з reduced-motion все ще бачать блиск

Корінь: відсутні або перевизначені правила @media (prefers-reduced-motion); блиск реалізовано так, що його не перекривають ці правила.

Виправлення: явно вимкніть keyframe-анімації і приберіть псевдо-елемент у режимі reduce; перевірте через налаштування ОС і емуляцію браузера.

5) Симптом: на десктопі продуктивність нормальна, на мобільному — жахлива

Корінь: мобільні GPU й термічні ліміти більш обмежені; екрани з високим DPI збільшують вартість paint/composite; занадто багато одночасно анімованих елементів.

Виправлення: обмежте анімацію до видимих вище згину заповнювачів; паузуйте блиск поза вьюпортом; знижуйте контраст і розмір смуги блиску.

6) Симптом: скелет мерехтить або показує шви під час анімації

Корінь: артефакти субпіксельного рендерингу від трансформів, особливо на масштабованих елементах або дробових ширинах.

Виправлення: використовуйте цілочисельні піксельні розміри де можливо; уникайте масштабування контейнерів скелета; розгляньте transform: translate3d(...) лише якщо це реально допомагає на ваших цільових браузерах.

7) Симптом: використання CPU залишається високим навіть коли сторінка в стані idle

Корінь: нескінченні анімації на багатьох елементах; відсутність умов зупинки; анімації, що працюють поза вьюпортом; вкладка не тротлиться через активне аудіо/відео або інші фактори.

Виправлення: зупиняйте блиск, коли дані прийшли; використовуйте мінімальний JS, щоб призупиняти анімації поза вьюпортом; зменшуйте кількість скелетів і уникайте глобального блиску.

8) Симптом: скелет «відчувається повільнішим», ніж спінер

Корінь: скелет з’явився, але контент займає стільки часу, що блиск стає фокусом уваги; користувач сприймає його як «застрягло».

Виправлення: знизьте інтенсивність блиску, додайте поступове рендерення реального контенту і виправте справжню бекенд/кеш-продуктивність. Скелети мають покривати коротку невизначеність, а не довге страждання.

Чеклисти / покроковий план

Покроково: випустіть лоудер скелета, про який не пошкодуєте

  1. Зробіть інвентар станів завантаження. Визначте, де користувачі чекають: перше завантаження, переходи маршрутів, часткові завантаження компонентів.
  2. Визначте контракти компонентів. Для кожного скелета вкажіть висоту, поведінку ширини, відступи і які частини блимають (якщо взагалі).
  3. Почніть зі статичного. Побудуйте скелети без анімації. Підтвердіть стабільність макета і прийнятний вигляд.
  4. Додайте блиск як покращення. Віддавайте перевагу sweep псевдо-елемента з transform.
  5. Поважайте зменшену анімацію. Вимикайте блиск під prefers-reduced-motion; тримайте заповнювачі видимими.
  6. Обмежте одночасні анімації. Блимають лише вище згину або лише перші N рядків списку.
  7. Швидко зупиняйте анімацію. Прибрати клас блиску відразу, як дані готові; також обробляти помилки і abort-шляхи.
  8. Вимірюйте CLS і FPS. Запустіть Lighthouse (CLS) і профайл продуктивності (paint/composite активність).
  9. Тестуйте на обмежених пристроях. Старі телефони, термічні умови, режими енергозбереження — реалістично, а не ідеально.
  10. Автоматизуйте регресії. Додайте CI-перевірку для CLS і хоча б один перформанс-трейс для маршруту з великою кількістю скелетів.

Чеклист: перешкоди продуктивності

  • Не анімуйте filter або великі box-shadow на скелетах.
  • Не анімуйте фон-градієнти на великих поверхнях, якщо ви не профілювали і не підтвердили, що це безпечно.
  • Не використовуйте will-change скрізь; обмежте і приберіть після завантаження.
  • Не блимайте контент поза вьюпортом; призупиняйте анімацію.
  • Підганяйте розміри скелета під фінальний макет, щоб зберегти CLS низьким.

Чеклист: вимоги доступності

  • Блоки скелета декоративні: сховайте їх від екранних читалок, якщо вони не передають значущого стану.
  • Ніколи не заганяйте фокус у стан скелета.
  • Послідовно дотримуйтеся prefers-reduced-motion у всіх компонентах.
  • Тримайте контраст скелета тонким; він не повинен маскуватися під вимкнений текст.

Поширені запитання

1) Чи кращі скелетні екрани за спінери?

Зазвичай так для UI з великою кількістю контенту. Скелети передають структуру і зменшують сприйняту затримку.
Спінери нормальні для коротких чекань або незв’язаних з макетом задач (наприклад, синхронізація у фоні), але вони не резервують простір і можуть здаватися «чекайте» без контексту.

2) Чи можна робити скелетні лоудери без JavaScript?

Ви можете рендерити скелети чистим CSS і прибирати їх на сервері, коли контент готовий. Але в клієнтських SPA зазвичай потрібен невеликий перемикач стану, щоб прибрати класи скелета.
Також призупинити блиск поза вьюпортом потребує JS (наприклад, IntersectionObserver), бо CSS не може надійно знати видимість.

3) Чому мій блиск викликає високий CPU, хоча це «лише CSS»?

Бо можливо ви змушуєте репейнти. Анімовані позиції градієнта, фільтри розмиття й великі області репейнту коштують CPU/GPU кожен кадр.
Віддавайте перевагу анімаціям на основі transform і тримайте площу анімації маленькою.

4) Чи завжди корисний will-change для блиску?

Ні. Він може допомогти для невеликої кількості анімованих елементів, дозволяючи просунути їх у шари.
Але при широкому застосуванні він збільшує використання пам’яті і може спричинити шаровий треш. Використовуйте його рідко і прибирайте після завершення анімації.

5) Скільки елементів скелета має блиматись одночасно?

Достатньо, щоб передати «завантаження», але не стільки, щоб нагріти залізо. Для списків блимайте 3–6 елементів над згином, інші тримайте статичними.
Якщо потрібне жорстке правило: почніть з 4 і збільшуйте лише після профайлингу на слабких пристроях.

6) Яка найкраща тривалість блиску?

Близько 1.1–1.6 секунд за повний прохід виглядає природно. Швидше виглядає нервово; повільніше — застряглим.
Більш важливо — припинити анімацію відразу, як контент готовий.

7) Як скелети взаємодіють з Core Web Vitals?

Правильно зроблені, скелети зменшують CLS, резервуючи простір. Неправильно — можуть підвищити CLS, якщо не відповідають фінальному макету.
Вони безпосередньо не покращують LCP, хіба що допомагають відрендерити значущий контент раніше; в основному вони покращують сприйняту продуктивність.

8) Чи повинні скелети імітувати точні рядки тексту й типографіку?

Імітуйте структуру, а не дрібниці. Використовуйте кілька блоків рядків з реалістичними ширинами і послідовними відступами.
Надто точні скелети крихкі: будь-яка зміна копі або типографіки призводить до розбіжностей макета і додаткових витрат на підтримку.

9) Який найпростіший підхід для reduced-motion?

У prefers-reduced-motion: reduce вимкніть keyframe-анімацію і приберіть шар блиску.
Залиште статичні заповнювачі. Це шанобливо і все ще інформативно.

10) Як зрозуміти, чи мій блиск bound-by-paint або bound-by-composite?

Профілюйте. Якщо ви бачите часті «Paint» події під час блиску, ви bound-by-paint. Якщо paint низький, але GPU зайнятий і у вас багато шарів, можливо bound-by-composite.
Практичний фікс схожий: зменшіть площу анімації і кількість елементів, анімуйте через transform.

Висновок: практичні наступні кроки

Скелетні екрани варті зусиль, але лише якщо ви ставитесь до них як до продакшн-коду, а не як до декорацій.
Побудуйте спочатку статичну базу. Додавайте блиск лише там, де він допомагає. Поважайте зменшену анімацію. І профілюйте на залозі, що не має вентилятора розміром з десертну тарілку.

Наступні кроки, які ви можете виконати цього тижня:

  • Замініть shimmer за позицією градієнта на sweep псевдо-елемента з transform у вашому топовому компоненті скелета.
  • Додайте prefers-reduced-motion перевизначення і перевірте їх через системні налаштування.
  • Обмежте блиск для заповнювачів над згином і зупиняйте його одразу при готовності даних.
  • Запустіть Lighthouse для CLS і трасування продуктивності на маршруті з довгим списком; фейл CI при очевидних регресіях.

Користувачі не надсилатимуть подяк за плавне завантаження. Вони просто припинять скаржитися. Ось і мрія.

← Попередня
WordPress 100% CPU: знайдіть плагін або бота, що навантажує сайт
Наступна →
SPF/DKIM проходять, але листи потрапляють у спам: приховані сигнали, які потрібно виправити

Залишити коментар