12k
All articles

boneyard による Skeleton Screen の自動生成

boneyard-jsは、実際のコンポーネントレイアウトから開発時にskeleton loaderを自動生成し、レスポンシブな.bones.jsonとVite対応を備えます。

OpenReplay Team
OpenReplay Team
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 を素早く反復しているチームにとって、このワークフローは実際に時間を節約し、ローディング状態を内部のコンポーネントに視覚的に忠実に保ちます。

よくある質問

boneyard-js は本番環境でランタイムオーバーヘッドを追加しますか?

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

boneyard-js は画面サイズをまたいだレスポンシブレイアウトをどう扱いますか?

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

キャプチャ中に CLI からアクセスできないデータを取得するコンポーネントの場合はどうなりますか?

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

生成された bones ファイルをバージョン管理にコミットできますか?

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

DevTools for the frontend

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.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.