boneyard による Skeleton Screen の自動生成
Skeleton ローダーは、実際に作ってみるまでは単純に見える UI パターンの一つです。DOM 要素を計測し、高さをハードコードし、条件付きレンダリングを組み立てる——そしてデザイナーがカードレイアウトを変更すれば、また同じ作業を繰り返すことになります。メンテナンスコストは静かに積み重なっていきます。
boneyard-js は異なるアプローチを取ります。Skeleton UI を手書きするのではなく、開発時にレンダリング済みのコンポーネントから直接レイアウトデータを抽出し、プレースホルダー定義を自動生成するのです。
重要なポイント
- boneyard-js は実際のコンポーネントからレイアウトデータをキャプチャして Skeleton ローダーを自動生成し、同じ UI を二重に保守する必要をなくします。
- DOM のスキャンは Playwright を介して開発時に実行され、静的な
.bones.jsonファイルが生成されます。本番環境ではランタイムでの DOM 走査は発生しません。 - キャプチャはデフォルトで 3 つのビューポート幅(375、768、1280px)で実行され、水平方向の値はレスポンシブ対応のためパーセンテージで保存されます。
fixtureprop と--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 に送信します。本番ビルドでは、このスキャンコードは完全に除外されます。
Discover how at OpenReplay.com.
動的コンテンツと 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 は、フレームワーク固有のアダプターを別パッケージのエクスポートとして提供します:
| フレームワーク | インポート |
|---|---|
| React | boneyard-js/react |
| Vue | boneyard-js/vue |
| Svelte 5 | boneyard-js/svelte |
| Angular | boneyard-js/angular |
| Preact | boneyard-js/preact |
| React Native | boneyard-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.