В этой статье в блоге ЛАНИТ хочу поделиться опытом нашей команды по внедрению скелетон-загрузки в приложение с семилетней историей. Мы столкнулись с плохой отзывчивостью нашего интерфейса и решили, что необходимо исправить эту проблему, чтобы улучшить жизнь пользователей, а заодно и сделать приложение более современным. 

Расскажу, как мы анализировали его для оптимизации трудозатрат, пошагово внедряли скелетон-загрузку и как это позволило нам получить хорошие отзывы от юзеров и визуально ускорить загрузку приложения. Также в статье расскажу о том, как мы поддерживаем и развиваем это решение.

Немного теории 

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

Индикаторы бывают точными (детерминированными) и абстрактными (недетерминированными).

Точные индикаторы показывают пользователю на каком этапе находится прогресс. Они являются более сложными в реализации, так как требуют расчёта процента или остаточного времени. 

Примеры таких индикаторов — прогресс-бар с процентом, круговой индикатор, ступенчатый индикатор.

Абстрактные индикаторы не дают точной информации о прогрессе, а просто показывают активный процесс. Они являются более простыми в реализации, так как требуют только отслеживания состояния загрузки.

Примеры таких индикаторов — спиннер, скелетон, прогресс-бар.

Подробнее про индикаторы загрузки и их выбор можно почитать здесь.

Мы будем реализовывать скелетон-загрузку, поэтому давайте изучим этот тип более подробно.

Скелетон — это набор заглушек, как правило, серых оттенков, имеющих пульсирующую или бегущую анимацию, которые точно или приблизительно повторяют структуру элемента, блока или всей страницы.

К плюсам такого типа индикации можно отнести следующее.

Повышение осведомленности пользователя. Благодаря отображению структуры страницы мы сразу показываем пользователю, как она будет выглядеть, и он может заранее сориентироваться в ней.

Плавный переход. Благодаря повторению структуры страницы смена “косточек” скелетона на контент происходит достаточно плавно и ожидаемо. Из этого вытекает ещё одно преимущество, которое заключается в улучшении метрики CLS, так как мы резервируем место под контент заранее.

Загрузка кажется быстрее. По результатам некоторых UX-исследований скелетон-загрузка воспринимается пользователями как что-то более быстрое, чем, например, спиннер или прогресс-бар. 

Это просто красиво. Достаточно субъективный плюс, но лично мне нравится, когда в приложении есть скелетон. Я люблю их рассматривать, но, возможно, это просто профдеформация.

Минусы тоже есть.

Сложность разработки. Данный тип индикации является одним из самых сложных, так как требует проработки элементов интерфейса при создании и поддержки при изменении.

Подходит не везде и не для всех. При выборе типа индикации необходимо понимать, что скелетон — не панацея и подойти он может не для всех случаев. Например, когда мы загружаем файл в течение пяти минут, лучше всего подойдёт прогресс-бар, или если мы не знаем, что именно будет показано на странице, то лучше не выдумывать скелетон для неё. Также для категории пользователей с вестибулярными расстройствами или повышенной чувствительностью к мерцанию множественная анимация скелетонов может вызывать дискомфорт.

С тем, что собой представляют скелетоны разобрались. Давайте приступим к их внедрению.

Работы перед внедрением

Анализируем приложение

Перед тем, как начинать писать код, мы решили подготовиться и провести полный аудит нашего приложения, чтобы сэкономить время на этапе разработки.

Приложение представляет собой админку для создания различных сущностей и настройки связей между ними и BI-систему, отображающую различные дашборды с аналитикой этих сущностей.

В рамках данного аудита мы проанализировали все страницы приложения на наличие состояний загрузки. Было выявлено, что у нас либо нет никакой индикации, либо есть большой спиннер на всю страницу или маленький внутри компонента.

Также мы узнали, какие страницы планируются к удалению, не используются или не будут развиваться. Где у нас есть схожие блоки, и из чего состоят те или иные страницы: из компонентов или сплошной вёрстки.

Далее мы определили порядок, в котором будем внедрять скелетон.

Продумываем решение

Этот шаг является самым ответственным, так как именно на нём будет закладываться фундамент будущего решения. 

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

  1. Библиотека с готовыми скелетон-компонентами или генерирующая их на основании DOM.

  2. Собственный набор скелетон-компонентов.

  3. Создание заглушек внутри компонентов.

  4. Добавление стилей скелетона или заглушек к существующей вёрстке.

Мы подробно рассмотрели каждый из вариантов и решили, что будем использовать.

Библиотека с готовыми скелетон-компонентами или генерирующая их на основании DOM. Данное решение кажется подходящим и достаточно простым: нашли самую популярную библиотеку, установили и пользуемся, но не всё так радужно. Любая библиотека — это и польза, и ограничение, и мы убедились в этом, посмотрев пару-тройку таких решений. Оказалось, что большинство возможностей нам не нужны и всё равно придётся делать свои скелетон-компоненты для специфических сценариев. Также мы в любом случае потеряем гибкость изменения предоставляемых компонентов и не хотим привязывать код к стороннему решению, поэтому этот вариант нам не подходит.

Собственный набор скелетон-компонентов. Изучив исходный код подходов и скелетон-компонентов из библиотек, стало ясно, что создать то же самое у себя не составит труда и при этом мы будем вольны делать всё, что захотим, а в проекте не появится лишней зависимости. Т. е. создав свой мини-аналог библиотеки для основных паттернов интерфейса, мы получим набор действительно нужных компонентов с необходимыми настройками.

Создание заглушек внутри компонентов. Наличие некоторого количества переиспользуемых скелетон-компонентов — это хорошо. Их можно использовать для повторяющихся паттернов интерфейса. Но у нас есть компоненты, для которых не подойдёт готовый скелетон-компонент. В этой ситуации лучше иметь скелетон-версию компонента в непосредственной близости с ним. Такой подход позволит сделать достаточную детализацию и поддержку компонента вместе со скелетоном простой.

Добавление стилей скелетона или заглушек к существующей вёрстке. Перечисленные выше варианты покрывают переиспользуемую и схожую часть интерфейса, но часто в приложении встречается и уникальная вёрстка, которая не может быть переиспользована. В таком случае хорошим решением является добавление классов со стилями скелетона и задание минимальных размеров или таких же заглушек, как и для компонентов, к уже существующей вёрстке по условию загрузки.

Выбираем стилистику

Так как мы отказались от использования готового решения и будем делать всё самостоятельно, надо выбрать стиль и анимацию для нашего будущего скелетона. Важно понимать, что стили скелетона должны быть одинаковыми в рамках всего приложения, поэтому они должны быть глобальными. Вот такой внешний вид скелетона для каждой из тем у нас получился.

Все остальные изображения будут статичными, но учитывайте, что анимация там тоже присутствует
Все остальные изображения будут статичными, но учитывайте, что анимация там тоже присутствует
/* Light */
:root {
  --skeleton-background: #EEEEEE;
  --skeleton-shimmer: rgba(124, 124, 124, 0.4);
}

/* Dark */
:root {
  --skeleton-background: #545454;
  --skeleton-shimmer: rgba(219, 219, 219, 0.5);
}

/* Moon */
:root {
  --skeleton-background: #252835;
  --skeleton-shimmer: rgba(219, 219, 219, 0.5);
}

.skeleton {
  position: relative;
  background-color: var(--skeleton-background);
  padding: 0;
  left: 0 !important;
  border: none;
  overflow: hidden;
}

.skeleton::before {
  position: absolute;
  left: 0; content: "";
  height: 100%;
  width: 100%;
  background-image: linear-gradient(to right, #00000011 0%, var(--skeleton-shimmer) 50%, #00000011 100%);
  background-size: 550px 400px;
  animation: shimmer 3s ease infinite;
}

@keyframes shimmer {
  0% {
    opacity: 0.1;
    background-position: 500px 0;
  }
  50% {
    opacity: 0.6;
    background-position: -500px 0;
  }
  100% {
    opacity: 0.1;
    background-position: 500px 0;
  }
}

Также мы решили сформулировать набор правил, которым должно будет соответствовать решение. Не вижу смысла повторяться, есть хороший материал, в котором дизайнер собрал большое количество правил проектирования скелетонов. Рекомендую.

Начинаем внедрять

Компоненты

Начать внедрение было решено с компонентов, так как они являются отдельными единицами интерфейса и используются во многих местах. Именно поэтому мы точно не сломаем никакой связанный код, сразу распространим скелетоны по разным местам приложения и сможем двигаться дальше по всем страницам.

Для компонентов мы будем использовать несколько вариантов реализации. В основном это Создание заглушек внутри компонентов и Добавление стилей скелетона или заглушек к существующей вёрстке, иногда может пригодиться использование компонента из Собственного набора скелетон-компонентов.

Мы прошлись по всем компонентам и стало понятно, что некоторым вовсе не нужны индикаторы загрузки, а для тех, которым нужны, они были реализованы (скелетон или спиннер). Ниже представлены примеры некоторых из них с кодом реализации.

Линейный график (Создание заглушек внутри компонентов)

Для отрисовки этого компонента используется canvas, поэтому по состоянию загрузки была сделана похожая на график заглушка с основной частью графика и значениями по осям.

<div>
  <div class="line-chart-skeleton">
    <div class="line-chart-y-labels-skeleton">
      <div class="line-chart-y-label-skeleton skeleton"></div>
      <div class="line-chart-y-label-skeleton skeleton"></div>
      <div class="line-chart-y-label-skeleton skeleton"></div>
      <div class="line-chart-y-label-skeleton skeleton"></div>
    </div>
    <div class="line-chart-body-skeleton skeleton"></div>
  </div>
  <div class="line-chart-x-labels-skeleton">
    <div class="line-chart-x-label-skeleton skeleton"></div>
    <div class="line-chart-x-label-skeleton skeleton"></div>
    <div class="line-chart-x-label-skeleton skeleton"></div>
  </div>
</div>
.line-chart-skeleton {
  display: flex;
}

.line-chart-y-labels-skeleton {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  margin-right: 4px;
  margin-bottom: 40px;
}

.line-chart-y-label-skeleton {
  width: 20px;
  height: 16px;
  border-radius: 6px;
}

.line-chart-body-skeleton {
  display: flex;
  align-items: center;
  width: 100%;
  height: 380px;
  border-radius: 6px;
}

.line-chart-x-labels-skeleton {
  display: flex;
  justify-content: space-between;
  margin-top: 4px;
  margin-left: 50px;
}

.line-chart-x-label-skeleton {
  width: 50px;
  height: 16px;
  border-radius: 6px;
}

Круговой график (Добавление стилей скелетона или заглушек к существующей вёрстке)

Данный компонент также использует canvas для отрисовки, но имеет обёртку с фиксированными размерами, на которую удалось просто добавить класс skeleton по условию загрузки.

<div class="line-chart-wrapper"
     [class.skeleton]="loading">
  <canvas #chartCanvas></canvas>
</div>
.doughnut-chart-wrapper {
  height: 260px;
  width: 260px;
  border-radius: 50%;
}

Вкладки (Собственный набор скелетон-компонентов)

В приложении был создан компонент для произвольного скелетон-блока, который получилось использовать во многих местах:

<div class="custom-block-skeleton skeleton"></div>
.custom-block-skeleton {
  border-radius: 15px;
  height: inherit;
  width: inherit;
  margin: inherit;
}

В компоненте вкладок он был использован следующим образом:

<app-custom-block-skeleton class="tab-skeleton"/>
<app-custom-block-skeleton class="tab-skeleton"/>
<app-custom-block-skeleton class="tab-skeleton"/>
.tab-skeleton {
  height: 50px;
  border-radius: 8px;

  &:nth-child(3n+1) {
    width: 110px;
  }

  &:nth-child(3n+2) {
    width: 100px;
  }

  &:nth-child(3n+3) {
    width: 120px;
  }
}

Также скелетоны были реализованы и для других компонентов, покажу некоторые из них без кода, так как везде он однотипный и несложный.

Кнопка

Теги

Таблица

Динамическая форма

Дерево

Как я говорил ранее, в некоторых местах, в том числе и компонентах, уже были небольшие спиннеры, и где-то мы добавили их сейчас. Это именно те ситуации, когда скелетон не подходит.

Календарь

Поле ввода

Выбор цвета

Компоненты скелетонов

Когда в самом начале мы прошлись всем страницам приложения, которые нуждаются в индикации загрузки, стали понятны повторяющиеся паттерны интерфейса, под которые необходимо создать переиспользуемые и настраиваемые компоненты.

Вот пример скелетон-компонента вертикальных карточек со всеми настройками, которые отчасти присущи и другим компонентам.

// Карточки сгруппированы (например, по дате)
@Input() grouped: boolean;
// Высота карточки больше базовой
@Input() large: boolean;
// Высота карточки меньше базовой
@Input() small: boolean;
// Количество карточек
@Input() count = 3;

cards: unknown[] = [];

ngOnInit(): void {
  this.cards = new Array(this.count);
}
<div class="cards-container-skeleton"
     [class.large]="large"
     [class.small]="small">
  <div *ngIf="grouped"
       class="cards-header-skeleton skeleton">
  </div>

  <div *ngFor="let _ of cards"
       class="card-skeleton skeleton">
  </div>
</div>
.cards-container-skeleton {
  height: inherit;
  margin: inherit;
  padding: inherit;
  overflow: scroll;
}

.cards-header-skeleton {
  height: 35px;
  width: 200px;
  border-radius: 15px;
  margin-bottom: 15px;
}

.card-skeleton {
  height: 180px;
  border-radius: 15px;
  margin-bottom: 15px;
}

.cards-container-skeleton.large .card-skeleton {
  height: 330px;
}

.cards-container-skeleton.small .card-skeleton {
  height: 92px;
  margin-bottom: 10px;
}

Другие примеры скелетон-компонентов покажу без кода, так как он везде однотипный.

Чипы

Список

Горизонтальные карточки

Квадратные блоки

Страницы

После создания скелетон-компонентов и покрытия индикаторами загрузки обычных компонентов, большинство страниц уже обрело новый вид, и осталось пройти по ним всем и сделать так, чтобы каждая страница по необходимости отображала все состояния загрузки.

Для страниц также было использовано несколько вариантов реализации. В основном это использование компонента из Собственного набора скелетон-компонентов и Добавление стилей скелетона или заглушек к существующей вёрстке.

Вот примеры некоторых получившихся страниц.

Страница с дашбордом

На этой странице используется ранее проработанный компонент таблицы, созданный скелетон-компонент горизонтальных карточек и недетализированный блок с добавлением стилей скелетона.

Страница с созданием данных

На этой странице используются ранее проработанные компоненты таблицы и динамической формы. Доработки не понадобились — благодаря компонентам она сама приняла такой вид.

Страница с аналитикой

На этой странице используются недетализированные блоки с добавлением стилей скелетона и минимальных размеров, так как детализация здесь была не нужна.

Страница с карточками и общими данными

На этой странице используются ранее проработанные компоненты графиков и чипов, созданный скелетон-компонент вертикальных карточек и детализированный блок с добавлением стилей скелетона.

Страница с детальными данными

На этой странице используются ранее проработанные компоненты графика с подробной легендой, созданные скелетон-компоненты горизонтальных карточек и квадратных блоков и достаточно детализированные блоки с добавлением стилей скелетона.

После проработки всех страниц мы собрались всей фронтенд-командой, ещё раз посвятили всех в то, что было сделано, и обсудили, как уменьшить количество вопросов и проблем в будущем.

Результаты и поддержка

В результате приложение стало значительно отзывчивее и приятнее в использовании. Это подтвердили положительные отзывы от наших пользователей, которым состояние приложения стало намного понятнее.

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

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

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

Надеюсь вам поможет наш опыт и вы пройдёте этот путь быстрее, чем могли бы. Всем отзывчивых интерфейсов!

Если вам интересно периодически получать заметки и кейсы из работы фронтенд-разработчика, подписывайтесь на мой телеграм-канал Даня про Frontend.

Статья написана в рамках ХабраЧелленджа 6.0, который прошел в ЛАНИТ весной 2026 года. О том, что такое ХабраЧеллендж, читайте здесь.

Комментарии (0)