Back

Автоматическая генерация скелетон-экранов с помощью boneyard

Автоматическая генерация скелетон-экранов с помощью boneyard

Скелетон-загрузчики — один из тех UI-паттернов, которые выглядят простыми, пока вы не возьмётесь их реализовывать. Вы измеряете DOM-элементы, хардкодите высоты, настраиваете условный рендеринг — а потом дизайнер меняет вёрстку карточки, и вы делаете всё заново. Стоимость поддержки незаметно растёт.

boneyard-js предлагает иной подход: вместо того чтобы писать скелетон-UI вручную, инструмент извлекает данные о layout прямо из ваших отрендеренных компонентов на этапе разработки и автоматически генерирует определения плейсхолдеров.

Ключевые выводы

  • boneyard-js автоматически генерирует скелетон-загрузчики, захватывая данные layout из ваших реальных компонентов, что устраняет необходимость поддерживать два параллельных представления одного и того же UI.
  • Сканирование DOM выполняется на этапе разработки через Playwright, создавая статические файлы .bones.json. В продакшене обхода DOM в рантайме не происходит.
  • По умолчанию захват выполняется на трёх ширинах вьюпорта (375, 768, 1280px), причём горизонтальные значения сохраняются в процентах для адаптивного поведения.
  • Проп fixture и флаг --wait решают проблему компонентов, зависящих от асинхронных данных, недоступных во время захвата.
  • Vite-плагин автоматически синхронизирует bones при каждом HMR-обновлении, избавляя от необходимости отдельного CLI-шага.
  • Адаптеры существуют для React, Vue, Svelte 5, Angular, Preact и React Native, и все они используют единый базовый формат.

Проблема ручных скелетон-загрузчиков

Большинство реализаций скелетон-загрузчиков оторваны от реального UI. Вы создаёте сам компонент, а затем отдельно собираете скелетон, приблизительно повторяющий его форму. Когда компонент меняется, скелетон отстаёт. Со временем ваши состояния загрузки перестают соответствовать контенту, что вызывает скачки layout и визуальную несогласованность.

Библиотеки вроде react-loading-skeleton уменьшают объём шаблонного кода, но всё равно требуют вручную описывать структуру. Вы по-прежнему поддерживаете два представления одного компонента.

Как boneyard-js автоматически генерирует скелетон-экраны

boneyard-js переворачивает рабочий процесс. Вы оборачиваете свой реальный компонент в тег <Skeleton>, запускаете CLI-команду — и инструмент сам захватывает layout.

Вот как это выглядит в React:

import { Skeleton } from 'boneyard-js/react'

function ActivityPanel() {
  const { data, isLoading } = useFetch('/api/activity')

  return (
    <Skeleton name="activity" loading={isLoading}>
      {data && <ActivityContent data={data} />}
    </Skeleton>
  )
}

Затем, при запущенном dev-сервере:

npx boneyard-js build

CLI открывает headless-браузер через Playwright, заходит в ваше приложение, находит каждый элемент <Skeleton name="..."> и обходит DOM-дерево внутри него. Он использует getBoundingClientRect() на листовых узлах — текстовых элементах, изображениях, кнопках, полях форм — и записывает их позицию и размер относительно корня скелетона. Горизонтальные значения сохраняются в процентах для адаптивности, а вертикальные — в пикселях. Border radius определяется автоматически.

Этот процесс по умолчанию выполняется на трёх ширинах вьюпорта (375, 768, 1280px), создавая по файлу .bones.json на каждый компонент:

{
  "breakpoints": {
    "375": {
      "bones": [
        { "x": 0, "y": 32, "w": 43.59, "h": 34, "r": 8 },
        { "x": 43.59, "y": 39, "w": 23.76, "h": 33, "r": 999 }
      ]
    }
  }
}

Также генерируется файл реестра (registry.js или registry.ts). Импортируйте его один раз в точке входа приложения — и все определения скелетонов станут доступны глобально:

import './bones/registry'

В рантайме компонент <Skeleton> считывает зарегистрированные данные bones по своему name, рендерит соответствующий layout как абсолютно позиционированные прямоугольники и подменяет их реальным контентом, когда loading становится false.

Захват на этапе сборки, а не сканирование в рантайме

Важный нюанс: сканирование DOM происходит во время разработки, а не в продакшене. Файлы .bones.json — это статические артефакты, которые коммитятся вместе с исходным кодом. В рантайме boneyard-js только читает эти заранее сгенерированные определения. Никакого живого обхода DOM в браузере не происходит.

Для React Native механизм захвата отличается. Компонент <Skeleton> сканирует fiber-дерево в dev-режиме с помощью UIManager, измеряет нативные представления и передаёт эти данные в CLI. В продакшен-сборках этот код сканирования полностью исключается.

Работа с динамическим контентом и проп fixture

Если ваш компонент зависит от API, недоступного во время захвата CLI, скелетон может сгенерироваться некорректно, потому что область контента пуста. Решить это можно двумя способами:

Используйте --wait, чтобы отложить захват после загрузки страницы:

npx boneyard-js build --wait 2000

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

<Skeleton
  name="activity"
  loading={isLoading}
  fixture={<ActivityContent data={mockData} />}
>
  {data && <ActivityContent data={data} />}
</Skeleton>

Содержимое fixture рендерится только во время захвата CLI и не оказывает влияния в продакшене.

Внимание: Если ваш компонент ничего не рендерит, пока data равно undefined, обёрточный элемент схлопывается до нулевой высоты, и скелетон не отобразится. Установите minHeight на <Skeleton>, чтобы избежать этого.

Vite-плагин для более тесной интеграции

Для проектов на Vite можно вообще обойтись без отдельного CLI-терминала:

// vite.config.ts
import { defineConfig } from 'vite'
import { boneyardPlugin } from 'boneyard-js/vite'

export default defineConfig({
  plugins: [boneyardPlugin()]
})

Bones захватываются при запуске dev-сервера и автоматически перезахватываются при каждом HMR-обновлении. Это позволяет держать ваши скелетон-загрузчики этапа сборки в синхронизации с UI без какого-либо ручного вмешательства.

Поддержка фреймворков

boneyard-js поставляет специфичные для фреймворков адаптеры в виде отдельных экспортов пакета:

ФреймворкИмпорт
Reactboneyard-js/react
Vueboneyard-js/vue
Svelte 5boneyard-js/svelte
Angularboneyard-js/angular
Preactboneyard-js/preact
React Nativeboneyard-js/native

Базовая логика извлечения и формат .bones.json едины для всех.

Стоит ли внедрять boneyard-js?

boneyard-js — относительно новый инструмент, поэтому стоит ожидать эволюции API. Тем не менее основная идея здравая: генерировать скелетон-UI из реальных данных layout, а не поддерживать его вручную.

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

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

Заключение

Скелетон-загрузчики не должны расходиться с компонентами, которые они представляют, но при ручной реализации это происходит почти всегда. boneyard-js решает проблему, рассматривая скелетоны как сгенерированный артефакт, а не написанный руками. Шаг захвата выполняется в dev-режиме, на выходе получается статический JSON-файл, а накладные расходы в рантайме минимальны. Для команд, быстро итерирующих над UI, такой рабочий процесс экономит реальное время и сохраняет визуальную точность состояний загрузки относительно базовых компонентов.

Часто задаваемые вопросы

В продакшене сканирование DOM не выполняется. CLI или Vite-плагин генерируют статические файлы .bones.json на этапе разработки. В продакшене компонент Skeleton читает эти определения и рендерит абсолютно позиционированные прямоугольники, добавляя лишь незначительные затраты в рантайме.

По умолчанию он захватывает bones на трёх ширинах вьюпорта: 375, 768 и 1280 пикселей. Горизонтальные позиции и ширины сохраняются в процентах, чтобы скелетон плавно масштабировался между брейкпоинтами, а вертикальные значения остаются в пикселях для предсказуемых отступов. В рантайме выбирается ближайший подходящий брейкпоинт исходя из текущей ширины вьюпорта.

У вас есть два варианта. Флаг --wait откладывает захват после загрузки страницы, чтобы дать асинхронным запросам время разрешиться. Альтернативно, проп fixture принимает мок-версию вашего компонента со статическими данными, которая рендерится только во время захвата CLI. Оба подхода обеспечивают, что скелетон отражает заполненный layout, а не пустой контейнер.

Да, и это рекомендуется. Файлы .bones.json и registry.js — это статические артефакты, описывающие ваши скелетон-layout. Коммит их в репозиторий синхронизирует команду, делает сборки воспроизводимыми без запуска шага захвата в CI и позволяет на code review замечать неожиданные изменения layout вместе с правками компонентов, которые их вызвали.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay