公開日 2026-04-06

LaravelでBlade / Livewire / Inertia をどう使い分けるか

Laravel 13 の共通ベース app に同じ Book 一覧の Blade / Livewire / Inertia ページを足し、責務の違いと選び分けを整理する。

目次

  1. 前提環境
  2. 1. ゴールと非対象
  3. 2. 先に結論を置く
  4. 3. 共通の土台を一度だけ作る
  5. 3-1. fresh app を作成する
  6. 3-2. compose.yml を作成する
  7. 3-3. .env の該当箇所を更新する
  8. 3-4. 共通の Book model を用意する
  9. 3-5. migration と sample data を入れる
  10. 4. Blade で始める最小形
  11. 5. Livewire が向く場面
  12. 6. Inertia へ分岐する条件
  13. 7. チーム構成と保守性で整理する
  14. 8. まとめ

Laravel で画面を作るとき、先に決めたいのは「UI をどこまで PHP 側で持つか」です。Blade のまま進めるのか、Livewire を差し込むのか、Inertia で Vue / React 側へ寄せるのかで、必要な依存、確認ポイント、保守の重さが変わります。

この記事では Laravel 13 の fresh app を 1 つだけ作り、同じ Book 一覧を Blade / Livewire / Inertia の別ページとして並べます。共通の土台は 1 回だけ作り、各方式で何が増えるかを見る構成です。既存記事は補助導線として参照しますが、前提にはしません。

対象読者: Laravel の Route / Controller / View の基本は追えるが、Blade / Livewire / Inertia の選び分けで止まりやすい人。

前提環境

  • Windows 11
  • WSL2(Ubuntu)
  • VS Code(Remote - WSL)
  • Docker Desktop
  • composer:2 コンテナ
  • node:24 コンテナ
  • Laravel 13
  • Livewire 4
  • Inertia.js + Vue 3
  • SQLite

以降のコマンドは、特記がない限り WSL 側ターミナルで実行します。

1. ゴールと非対象

この記事で到達する状態は次のとおりです。

  • Blade / Livewire / Inertia の違いを「描画責務」と「チーム前提」で説明できる
  • Laravel 13 の共通ベース app に 3 つの最小ページを足す流れを追える
  • compose.yml.envartisantinkerroute:list、画面確認で何を見るべきか分かる
  • 自分の画面要件なら何を選ぶか判断できる

今回は比較と最小確認に絞ります。次の内容は扱いません。

  • Breeze / Starter Kit 全体の詳細比較
  • Livewire Volt、Flux UI、Alpine.js の個別説明
  • React 版 Inertia の実装差分
  • テスト、CI、SSR、デプロイ
  • Sanctum と組み合わせた API 設計

2. 先に結論を置く

一文で先に切ると、Blade はサーバー描画の第一候補、Livewire は Blade の中に動的 UI を差し込む選択肢、Inertia は Laravel の backend を保ったままページ単位の SPA 体験を作る選択肢です。

画面要件選びやすい候補理由
一覧・詳細・作成画面を素直に返したいBladeroute / controller / view の流れが最も単純だから
Blade を保ったまま検索やモーダルを足したいLivewirePHP 側の component で状態を持てるから
Vue / React でページ全体を組みたいInertiarouting / auth は Laravel のまま、描画はフロントへ寄せられるから

判断フローを図にすると次の形です。

flowchart TD
    A[作りたい画面] --> B{full page reload で十分か}
    B -->|yes| C[Blade]
    B -->|no| D{動的なのは画面の一部か}
    D -->|yes| E[Livewire]
    D -->|no| F{Vue / React のページとして育てたいか}
    F -->|yes| G[Inertia]
    F -->|no| E

Blade から始めるのが悪いわけではありません。むしろ最初に選びやすい起点です。画面の一部だけが動的なら Livewire、ページ全体をフロントコンポーネントとして持ちたいなら Inertia と分けると迷いが減ります。

3. 共通の土台を一度だけ作る

Blade / Livewire / Inertia の違いを見やすくするため、今回は Laravel app を 1 つだけ作り、その上に 3 つのページを足して比べます。作業ディレクトリは ~/projects/laravel-choice-demo で統一し、確認 URL ごとに分ける構成です。

方式追加するページ確認 URL
Blade/blade/bookshttp://localhost:8000/blade/books
Livewire/livewire/bookshttp://localhost:8000/livewire/books
Inertia/inertia/bookshttp://localhost:8000/inertia/books

まずはこの共通土台を 1 回だけ作ります。

3-1. fresh app を作成する

mkdir -p ~/projects/laravel-choice-demo
cd ~/projects/laravel-choice-demo
code .

docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app composer:2 create-project laravel/laravel .

3-2. compose.yml を作成する

compose.yml は次の内容で作成します。

services:
  app:
    image: composer:2
    user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
    working_dir: /app
    environment:
      HOME: /tmp
      XDG_CONFIG_HOME: /tmp/.config
    volumes:
      - ./:/app
    ports:
      - "8000:8000"
    command: php artisan serve --host=0.0.0.0 --port=8000

この app サービスは 4 章から 6 章で共通です。

3-3. .env の該当箇所を更新する

.env の該当部分は次のとおりです。

APP_URL=http://localhost:8000
DB_CONNECTION=sqlite
DB_DATABASE=/app/database/database.sqlite

DB_DATABASE/app/database/database.sqlite にしておくと、コンテナ内からのパスが固定されます。

次を実行して起動してください。

export LOCAL_UID="$(id -u)"
export LOCAL_GID="$(id -g)"
docker compose up -d

3-4. 共通の Book model を用意する

3 方式とも同じ題材にそろえるため、Book model と migration を作ります。

docker compose exec app php artisan make:model Book -m

生成された database/migrations/*_create_books_table.php は次の内容に置き換えてください。timestamp 部分は実行時刻で変わります。

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('books', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('author');
            $table->unsignedInteger('price');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('books');
    }
};

app/Models/Book.php は次の内容です。

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    protected $fillable = [
        'title',
        'author',
        'price',
    ];
}

3-5. migration と sample data を入れる

docker compose exec app php artisan migrate
docker compose exec app php artisan tinker --execute="App\\Models\\Book::query()->create(['title' => 'Laravel Intro', 'author' => 'Aki Tanaka', 'price' => 2800]); App\\Models\\Book::query()->create(['title' => 'Livewire Search UI', 'author' => 'Mika Sato', 'price' => 3400]); App\\Models\\Book::query()->create(['title' => 'Inertia Vue Guide', 'author' => 'Ken Suzuki', 'price' => 3900]);"

ここまでが共通の土台です。4章では /blade/books、5章では /livewire/books、6章では /inertia/books を同じ app に足します。

4. Blade で始める最小形

Blade は、route / controller / view で素直に画面を返したいときの第一候補です。3章で用意した ~/projects/laravel-choice-demo の中で、まずは controller を作ります。

docker compose exec app php artisan make:controller BookPageController

app/Http/Controllers/BookPageController.php は次の内容です。

<?php

namespace App\Http\Controllers;

use App\Models\Book;
use Illuminate\Contracts\View\View;

class BookPageController extends Controller
{
    public function __invoke(): View
    {
        return view('books.index', [
            'books' => Book::query()->orderBy('title')->get(),
        ]);
    }
}

resources/views/books/index.blade.php は次の内容で作成します。

<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Blade Books</title>
    </head>
    <body>
        <h1>Blade で本一覧を描画する</h1>
        <ul>
            @foreach ($books as $book)
                <li>{{ $book->title }} / {{ $book->author }} / {{ number_format($book->price) }}円</li>
            @endforeach
        </ul>
    </body>
</html>

routes/web.php は次のとおりです。

<?php

use App\Http\Controllers\BookPageController;
use Illuminate\Support\Facades\Route;

Route::redirect('/', '/blade/books');
Route::get('/blade/books', BookPageController::class)->name('blade.books');

ルートを確認します。

docker compose exec app php artisan route:list --name=blade.books

HTTP 応答も手元で確かめておいてください。

curl http://localhost:8000/blade/books

curl すると、サーバー HTML の中にそのまま <h1>Blade で本一覧を描画する</h1> が出ます。route / controller / view を追えば、何が返るかをそのまま読めます。

ここまでできたら、ブラウザで http://localhost:8000/blade/books を開いて、3 件の本一覧が見えていることも確認してください。

Blade が向くのは、一覧・詳細・作成画面をサーバー描画で十分まわせる場面です。ページ全体の責務が PHP 側に閉じるので、最初の実装も保守の入り口も最も単純になります。

Blade の /blade/books 画面で 3 件の本一覧が表示されている様子

5. Livewire が向く場面

Livewire は、Blade を捨てずに画面の一部だけ動的にしたいときの選択肢です。ここでは 3章の土台に package と page を足します。

docker compose exec app composer require livewire/livewire

app/Livewire/BooksIndex.php を次の内容で作成します。

<?php

namespace App\Livewire;

use App\Models\Book;
use Illuminate\Contracts\View\View;
use Livewire\Component;

class BooksIndex extends Component
{
    public string $search = '';

    public function clearSearch(): void
    {
        $this->reset('search');
    }

    public function render(): View
    {
        return view('livewire.books-index', [
            'books' => Book::query()
                ->when($this->search !== '', fn ($query) => $query->where('title', 'like', '%' . $this->search . '%'))
                ->orderBy('title')
                ->get(),
        ]);
    }
}

resources/views/livewire/books-index.blade.php は次の内容です。

<div>
    <div>
        <input wire:model.live="search" type="text" placeholder="タイトルで検索">
        <button wire:click="clearSearch" type="button">クリア</button>
    </div>
    <p>検索語: {{ $search === '' ? 'なし' : $search }}</p>
    <ul>
        @foreach ($books as $book)
            <li>{{ $book->title }} / {{ $book->author }} / {{ number_format($book->price) }}円</li>
        @endforeach
    </ul>
</div>

Livewire component を埋め込む wrapper page として、resources/views/livewire/books-page.blade.php は次の内容で作成します。

<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Livewire Books</title>
        @livewireStyles
    </head>
    <body>
        <h1>Livewire で小さな動的UIを足す</h1>
        <livewire:books-index />
        @livewireScripts
    </body>
</html>

routes/web.php には、Blade page に加えて Livewire page を足します。

<?php

use App\Http\Controllers\BookPageController;
use Illuminate\Support\Facades\Route;

Route::redirect('/', '/blade/books');
Route::get('/blade/books', BookPageController::class)->name('blade.books');
Route::view('/livewire/books', 'livewire.books-page')->name('livewire.books');

確認してください。

docker compose exec app php artisan route:list --name=livewire.books
curl http://localhost:8000/livewire/books

curl すると、サーバー HTML の中に <h1>Livewire で小さな動的UIを足す</h1> が出ます。Blade と同じように HTML を返しつつ、検索欄の wire:model.liveclearSearch() の呼び出しだけが component 経由で動く構成です。

ここまでできたら、ブラウザで http://localhost:8000/livewire/books を開き、検索欄へ入力したときに一覧が絞り込まれることも確認してください。

Livewire は、ページ全体を JS ページへ寄せるのではなく、Blade に component を差し込んで動的な部分だけ増やす構成です。検索、モーダル、簡単なインライン更新のように「画面全体を SPA にするほどではない」場面向きです。

Livewire の /livewire/books 画面で「Livewire」と入力し、一覧が 1 件に絞り込まれた様子

6. Inertia へ分岐する条件

Inertia を選ぶと、Laravel 側の route / controller / middleware はそのまま使いながら、描画の主役は Vue / React に移ります。ここでは同じ app に server-side adapter と frontend build を足します。

docker compose exec app composer require inertiajs/inertia-laravel
docker compose exec app php artisan inertia:middleware

bootstrap/app.php は次の内容です。

<?php

use App\Http\Middleware\HandleInertiaRequests;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware): void {
        $middleware->web(append: [
            HandleInertiaRequests::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions): void {
        //
    })->create();

root template と client entry を用意します。resources/views/app.blade.php は次の内容で作成します。

<!doctype html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        @vite(['resources/css/app.css', 'resources/js/app.js'])
        @inertiaHead
    </head>
    <body>
        @inertia
    </body>
</html>

既存の resources/js/app.js は次の内容に更新してください。

import '../css/app.css'
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'

createInertiaApp({
    resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
    setup({ el, App, props, plugin }) {
        createApp({ render: () => h(App, props) })
            .use(plugin)
            .mount(el)
    },
})

既存の resources/css/app.css は次の内容に更新します。.vue の走査対象が抜けると、Index.vue に書いた Tailwind utility class が build 後の CSS へ出ません。

@import 'tailwindcss';

@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
@source '../**/*.js';
@source '../**/*.vue';

@theme {
    --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
        'Segoe UI Symbol', 'Noto Color Emoji';
}

resources/js/Pages/Books/Index.vue は次の内容で作成してください。

<script setup>
defineProps({
    books: {
        type: Array,
        required: true,
    },
})

const priceFormatter = new Intl.NumberFormat('ja-JP')
</script>

<template>
    <main class="min-h-screen bg-slate-100 px-6 py-10 text-slate-900">
        <div class="mx-auto max-w-3xl space-y-6">
            <header class="space-y-2">
                <p class="text-xs font-semibold uppercase tracking-[0.2em] text-sky-700">Inertia + Vue</p>
                <h1 class="text-3xl font-semibold tracking-tight">Inertia で画面単位のSPA体験を作る</h1>
                <p class="text-sm leading-6 text-slate-600">Laravel は props を返し、見た目の組み立ては Vue 側が担当します。</p>
            </header>
            <ul class="grid gap-3">
                <li
                    v-for="book in books"
                    :key="book.id"
                    class="rounded-2xl border border-slate-200 bg-white px-5 py-4 shadow-sm shadow-slate-200/60"
                >
                    <p class="text-lg font-medium">{{ book.title }}</p>
                    <div class="mt-2 flex flex-wrap items-center gap-2 text-sm text-slate-600">
                        <span>{{ book.author }}</span>
                        <span class="text-slate-300">/</span>
                        <span>{{ priceFormatter.format(book.price) }}円</span>
                    </div>
                </li>
            </ul>
        </div>
    </main>
</template>

Laravel fresh app に最初からある vite.config.js は、tailwindcss() を残したまま Vue plugin も足して次の内容に更新します。

import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
import laravel from 'laravel-vite-plugin'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
    plugins: [
        tailwindcss(),
        laravel({
            input: ['resources/css/app.css', 'resources/js/app.js'],
            refresh: true,
        }),
        vue(),
    ],
})

routes/web.php には、Blade と Livewire に加えて Inertia page を足します。

<?php

use App\Http\Controllers\BookPageController;
use App\Models\Book;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;

Route::redirect('/', '/blade/books');
Route::get('/blade/books', BookPageController::class)->name('blade.books');
Route::view('/livewire/books', 'livewire.books-page')->name('livewire.books');
Route::get('/inertia/books', function () {
    return Inertia::render('Books/Index', [
        'books' => Book::query()->orderBy('title')->get(['id', 'title', 'author', 'price']),
    ]);
})->name('inertia.books');

frontend 依存と build を追加してください。

docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app node:24 npm install
docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app node:24 npm install vue @vitejs/plugin-vue @inertiajs/vue3
docker run --rm -u "$(id -u):$(id -g)" -v "$(pwd):/app" -w /app node:24 npm run build
docker compose exec app php artisan route:list --name=inertia.books
curl http://localhost:8000/inertia/books

ここで Blade / Livewire と違う点が 2 つあります。

1 つ目は、Node.js と build が主線に入ることです。vite.config.js には tailwindcss()vue() の両方が必要です。どちらかが抜けると、Index.vue に class を書いても CSS へ出なかったり、Vue page が解決できなかったりします。Index.vue に class を書いたのに背景やカードが出ないときは、resources/css/app.css@source '../**/*.vue'; が入っているか、vite.config.jstailwindcss() が残っているかを最初に見てください。

2 つ目は、curl した初期 HTML の見え方です。<h1>Inertia で画面単位のSPA体験を作る</h1> は HTML に直接出ず、代わりに <script data-page> の JSON に Books/Indexbooks props が入り、本文描画は Vue 側が担当します。Inertia を選ぶと、ここで描画責務が Laravel 側からフロント側へ切り替わります。

ここまでできたら、ブラウザで http://localhost:8000/inertia/books を開き、Vue 側で本一覧が描画されていることも確認してください。

今回は描画責務の違いを見たいので、見た目は Tailwind utility class を少し足す程度に留めています。ブラウザで開いたときに、薄い背景の上へ本一覧がカード状に並んでいれば十分です。

Inertia の /inertia/books 画面でカード状の本一覧が表示されている様子

Inertia が向くのは、画面全体を Vue / React のページとして持ちたいときです。フォーム状態、ページ遷移、コンポーネント分割をフロント側で設計したいなら、Livewire を無理に伸ばすより整理しやすくなります。

7. チーム構成と保守性で整理する

ここまでを、画面要件とチーム前提で表にすると次のようになります。

観点BladeLivewireInertia
描画の主役server-side HTMLserver-side HTML + component 更新client-side page component
追加依存ほぼ増えないComposer package が増えるComposer + npm package + build が増える
向く画面シンプルな一覧、詳細、作成検索、モーダル、簡単な動的 UIダッシュボード、複雑なフォーム、ページ単位の SPA
チーム前提PHP 中心PHP 中心で少し動的さが欲しいVue / React を触れる人がいる
保守の勘所route / controller / view を追えばよいwire: 属性と対応する component class の property / method を見るroute が返す props と Pages/*.vuedefineProps() を対応づけて追う

判断をさらに短くすると、次の 3 行です。

  • full page reload で問題がないなら、まず Blade
  • 画面の一部だけが動的で、PHP 側の思考を保ちたいなら Livewire
  • 画面全体を Vue / React コンポーネントとして育てたいなら Inertia

保守性の観点では、「誰がどこを触るか」も重要です。PHP 中心のチームで管理画面を作るなら BladeLivewire のほうが踏み外しにくい構成になります。逆に、画面ごとの state 管理や component 分割をフロント側で進める前提があるなら、最初から Inertia に寄せたほうが責務がはっきりする構成です。

8. まとめ

Laravel で画面を作るときは、最初に Blade を基準に考えると整理しやすくなります。full page reload で十分ならそのまま進み、部分的な動的 UI が必要になったら Livewire、ページ全体を Vue / React で持ちたいなら Inertia へ進む流れです。

判断をシナリオで言い切るなら、次の 3 つです。

  • 管理画面の MVP を早く出したいなら、まず Blade
  • Blade のまま検索や小さな対話を増やしたいなら、Livewire
  • 画面が今後も増え、Vue / React の component として育てたいなら、最初から Inertia

次に読む候補としては、基礎から入り直すなら Laravel入門(Route / Controller / View / Model 最小構成)、Livewire を実際に深めるなら LaravelでLivewireを始める がつながります。認証付きの fresh app を official flow で確認したい場合は Laravel 13のStarter Kitsで認証付きアプリを始める も補助導線になります。

シリーズ 14/16

このシリーズ

Laravelの基本を最初から通す

  1. 1. Laravel入門(Route / Controller / View / Model 最小構成)
  2. 2. Laravelで最小CRUDを作る(一覧 / 作成 / 編集 / 削除)
  3. 3. Laravelで品質ゲートを敷く(PHPUnit / Larastan / Pint / GitHub Actions)
  4. 4. Laravelで認証を足す(Breeze 最小導入)
  5. 5. Laravelで認可を入れる(Policyで自分の本だけ編集できるようにする)
  6. 6. LaravelでQueueを始める(database queue + worker 最小構成)
  7. 7. Laravelでスケジューラを動かす(Command + Scheduler 最小構成)
  8. 8. Laravelでファイルアップロードを扱う(Storage + validation)
  9. 9. Laravelでリレーションを扱う(User / Book / Category の基本)
  10. 10. Laravelで検索・並び替え・ページネーション付き一覧を作る
  11. 11. LaravelでLivewireを始める
  12. 12. LaravelでLivewire一覧画面を作る(検索・並び替え・ページネーション)
  13. 13. LaravelでSanctum認証APIを作る
  14. 14. LaravelでBlade / Livewire / Inertia をどう使い分けるか 現在の記事
  15. 15. LaravelでInertia + Vue.js を始める
  16. 16. LaravelでInertia + React を始める