Back

boneyard による Skeleton Screen の自動生成

boneyard による Skeleton Screen の自動生成

Skeleton ローダーは、実際に作ってみるまでは単純に見える UI パターンの一つです。DOM 要素を計測し、高さをハードコードし、条件付きレンダリングを組み立てる——そしてデザイナーがカードレイアウトを変更すれば、また同じ作業を繰り返すことになります。メンテナンスコストは静かに積み重なっていきます。

boneyard-js は異なるアプローチを取ります。Skeleton UI を手書きするのではなく、開発時にレンダリング済みのコンポーネントから直接レイアウトデータを抽出し、プレースホルダー定義を自動生成するのです。

重要なポイント

  • boneyard-js は実際のコンポーネントからレイアウトデータをキャプチャして Skeleton ローダーを自動生成し、同じ UI を二重に保守する必要をなくします。
  • DOM のスキャンは Playwright を介して開発時に実行され、静的な .bones.json ファイルが生成されます。本番環境ではランタイムでの DOM 走査は発生しません。
  • キャプチャはデフォルトで 3 つのビューポート幅(375、768、1280px)で実行され、水平方向の値はレスポンシブ対応のためパーセンテージで保存されます。
  • fixture prop と --wait フラグは、キャプチャ時に取得できない非同期データに依存するコンポーネントに対応します。
  • Vite プラグインは HMR 更新のたびに bones を自動的に同期し、別途 CLI を実行する手間を省きます。
  • React、Vue、Svelte 5、Angular、Preact、React Native 向けのアダプターが用意されており、すべて同じコアフォーマットを共有しています。

手動で Skeleton ローダーを作る際の問題

Skeleton ローダーの実装の多くは、実際の UI から切り離されています。実コンポーネントを作り、その後に形を近似した Skeleton を別途構築します。コンポーネントが変わると、Skeleton はずれていきます。やがて、ローディング状態がコンテンツと一致しなくなり、レイアウトシフトや視覚的な不整合を引き起こします。

react-loading-skeleton のようなライブラリは定型コードを減らしてくれますが、それでも構造を手動で記述する必要があります。結局、同じコンポーネントの 2 つの表現を維持しなければなりません。

boneyard-js は Skeleton Screen をどのように自動生成するか

boneyard-js はワークフローを反転させます。実際のコンポーネントを <Skeleton> タグでラップし、CLI コマンドを実行すると、ツールがレイアウトをキャプチャしてくれます。

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>
  )
}

開発サーバーを起動した状態で:

npx boneyard-js build

CLI は Playwright 経由でヘッドレスブラウザを起動し、アプリにアクセスして、すべての <Skeleton name="..."> 要素を見つけ、その内部の DOM ツリーを走査します。リーフノード(テキスト要素、画像、ボタン、フォーム入力)に対して getBoundingClientRect() を使用し、Skeleton ルートからの相対位置とサイズを記録します。水平方向の値はレスポンシブ対応のためパーセンテージで、垂直方向の値はピクセルで保存されます。border-radius は自動的に検出されます。

このプロセスはデフォルトで 3 つのビューポート幅(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)も生成されます。アプリのエントリポイントで一度インポートするだけで、すべての Skeleton 定義がグローバルに利用可能になります:

import './bones/registry'

実行時には、<Skeleton> コンポーネントは name に対応する登録済みの bone データを読み取り、絶対配置された矩形としてレイアウトをレンダリングし、loading が false になったら実際のコンテンツに切り替えます。

ランタイムスキャンではなく、ビルド時キャプチャ

重要な区別として、DOM のスキャンは開発中に行われ、本番環境では行われません。.bones.json ファイルはソースコードと一緒にコミットされる静的成果物です。実行時には、boneyard-js はその事前生成された定義を読み取るだけで、ブラウザでのライブな DOM 走査は発生しません。

React Native ではキャプチャ機構が異なります。<Skeleton> コンポーネントは開発モードで UIManager を使ってファイバーツリーをスキャンし、ネイティブビューを計測してそのデータを CLI に送信します。本番ビルドでは、このスキャンコードは完全に除外されます。

動的コンテンツと fixture Prop の扱い

CLI のキャプチャ時に利用できない API にコンポーネントが依存している場合、コンテンツ領域が空のため Skeleton が誤って生成される可能性があります。これを解決する 2 つの選択肢があります:

ページ読み込み後にキャプチャを遅延させるための --wait:

npx boneyard-js build --wait 2000

キャプチャ専用に静的なモックデータを供給するための fixture prop:

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

fixture のコンテンツは CLI キャプチャ中にのみレンダリングされ、本番では何の影響もありません。

注意: data が undefined のときにコンポーネントが何もレンダリングしないと、ラッパー要素の高さが 0 に潰れ、Skeleton が表示されなくなります。これを防ぐには <Skeleton>minHeight を設定してください。

より緊密な統合のための Vite プラグイン

Vite ベースのプロジェクトでは、別の CLI ターミナルを完全に省略できます:

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

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

bones は開発サーバーの起動時にキャプチャされ、HMR 更新のたびに自動的に再キャプチャされます。これにより、手動操作なしでビルド時の Skeleton ローダーを 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 は今後も進化していくと予想されます。とはいえ、コア概念は確かなものです——Skeleton UI を手作業で維持するのではなく、実際のレイアウトデータから生成するというものです。

実用的なメリットは、コンポーネントが頻繁に変わるプロジェクトで最も明らかになります。デザインが変わるたびに Skeleton プレースホルダーを更新する代わりに、コマンドを 1 つ再実行するだけです。.bones.json ファイルが更新され、フロントエンドのローディング状態は正確に保たれます。

すでに Skeleton ローダーを実際の UI と同期させるのに時間を費やしているなら、自動化のセットアップコストはかける価値があります。

まとめ

Skeleton ローダーは、それが表すコンポーネントからずれてはいけないはずですが、手動の実装ではほぼ必ずずれが生じます。boneyard-js は、Skeleton を手書きの成果物ではなく生成された成果物として扱うことで、この問題に対処します。キャプチャステップは開発時に実行され、出力は静的な JSON ファイルで、ランタイムコストは最小限です。UI を素早く反復しているチームにとって、このワークフローは実際に時間を節約し、ローディング状態を内部のコンポーネントに視覚的に忠実に保ちます。

よくある質問

本番環境では DOM スキャンは発生しません。CLI または Vite プラグインが開発時に静的な .bones.json ファイルを生成します。本番では、Skeleton コンポーネントがそれらの定義を読み取り、絶対配置された矩形をレンダリングするだけで、わずかなランタイムコストしか追加されません。

デフォルトで 3 つのビューポート幅(375、768、1280 ピクセル)で bones をキャプチャします。水平位置と幅はパーセンテージで保存されるため、Skeleton はブレークポイント間で滑らかにスケールし、垂直方向の値は予測可能なスペーシングのためピクセルのまま保たれます。ランタイムでは、現在のビューポート幅に基づいて最も近いブレークポイントが選択されます。

2 つの選択肢があります。--wait フラグはページ読み込み後のキャプチャを遅延させ、非同期リクエストが解決する時間を与えます。あるいは、fixture prop で静的データを使ったモック版のコンポーネントを受け取り、CLI キャプチャ中にのみレンダリングされます。どちらのアプローチでも、Skeleton が空のコンテナではなく、データが入ったレイアウトを反映するようになります。

はい、コミットすべきです。.bones.json ファイルと registry.js は Skeleton レイアウトを記述する静的成果物です。これらをコミットすることでチームの足並みを揃えられ、CI でキャプチャステップを実行せずにビルドを再現でき、コンポーネントの編集と一緒に予期しないレイアウトの変更をコードレビューで検出できるようになります。

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