Nuxt Server Components を試す
気になったので試してみる。
まずはスキャフォールディング。
npx nuxi@latest init
設定ファイルと app.vue だけのごく質素なプロジェクトが生成された(こんな感じ)。前はもっと色々なファイルがあった気がする。慣れている人には余計なものがなくてよさそうだが、Nuxt を初めて使うような場合は途方に暮れそう。
公式ドキュメントに貼られている動画の内容を再現していく。
0:53
まずは通常通りにコンポーネントを作る。
これはマークダウンの文字列を HTML にパースして表示するコンポーネントで、変換には markdown-it
というファイルサイズが大きいライブラリを使う。
<script setup lang="ts">
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
const string = `# my component
- markdown list
- rendered as html
- cool
`;
const html = md.render(string);
</script>
<template>
<div v-html="html" />
</template>
<style scoped>
div {
color: #15ca82;
}
</style>
依存パッケージもインストール。
npm i markdown-it
npm i -D @types/markdown-it
ルートコンポーネントにマウント。
<template>
<MarkdownComponent />
</template>
dev サーバーを立ち上げ、ページを表示。
リクエストに 208kB の markdown-it.js
が含まれている。
トータルは 108 requests | 4.3 MB transferred | 4.2 MB resources
となっている。
ここからサーバーコンポーネントに変えていく。
1:37
現在、サーバーコンポーネントはまだ実験的機能なので、設定ファイルを修正して有効化する。
export default defineNuxtConfig({
devtools: { enabled: true },
+ experimental: {
+ componentIslands: true,
+ },
});
そしてコンポーネントファイルをリネームすれば変更完了。
MarkdownComponent.vue
↓
MarkdownComponent.server.vue
ブラウザーをリロード。
markdown-it.js
はなくなり、トータルも 96 requests | 3.8 MB transferred | 3.8 MB resources
と削減されている。
先程の結果からリクエストが 12 も減っていたり、容量も大幅に削減されていて単純な引き算になっていないのは dev サーバーだからか。
ちゃんとビルドして確認してみた。
サーバーコンポーネントにすると、CSS ファイルが 1 つなくなり、JS ファイルの容量が 95 kB 減っている。
クライアントコンポーネント MarkdownComponent.vue
バージョン
サーバーコンポーネント MarkdownComponent.server.vue
バージョン
2:12
サーバーコンポーネントにインタラクティブな処理を追加。
ボタンを押しても何も反応しない。
サーバー側で処理されるので、クライアント側には JS が配信されないため。
<script setup lang="ts">
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
const string = `# my component
- markdown list
- rendered as html
- cool
`;
const html = md.render(string);
+ const count = ref(0);
</script>
<template>
+ <button @click="count++">count is {{ count }}</button>
<div v-html="html" />
</template>
2:29
状態を親に移動し、コンポーネントには props を渡すように変更。
+ <script setup lang="ts">
+ const count = ref(0);
+ </script>
<template>
+ <button @click="count++">increment</button>
- <MarkdownComponent />
+ <MarkdownComponent :count="count" />
</template>
<script setup lang="ts">
import MarkdownIt from 'markdown-it';
+ defineProps<{
+ count: number;
+ }>();
const md = new MarkdownIt();
const string = `# my component
- markdown list
- rendered as html
- cool
`;
const html = md.render(string);
- const count = ref(0);
</script>
<template>
- <button @click="count++">count is {{ count }}</button>
+ <p>count is {{ count }}</p>
<div v-html="html" />
</template>
カウントは更新されるようになるが、ボタンを押す(props が更新される)たびにリクエストが飛び、サーバーで再計算した結果がブラウザーに反映されている。
これらの結果、動画では「サーバーコンポーネントはインタラクティブである必要がないコンポーネントや、頻繁に更新されないコンポーネントに使うのがよい」と言っている。
後述する通り、こういう場合はスロットを使うのがよさそう。
制限について。
動画は 2023/3/20 に投稿されたものでいくつかの情報は古くなっている。
制限1
3:18
非同期コンポーネントは使えない → v3.3.2 で解決済み。
制限2
3:24
スロットは使えない → v3.5 で解決済み。
制限3
3:27
(Next.js のように)サーバーコンポーネントの中にクライアントコンポーネントを置けない
これは現時点では未解決。
以下のようなコンポーネントを作って MarkdownComponent.server.vue
内に配置したが、ボタンを押してもカウントは増えなかった。(サーバーコンポーネント内に直接インタラクティブな処理を書いたときと同じ)
<script setup lang="ts">
const count = ref(0);
</script>
<template>
<button @click="count++">count is {{ count }}</button>
</template>
その代わり、スロットを使えばサーバーコンポーネントの中にクライアントコンポーネントを(擬似的に)置くことはできる。これにより、ボタンがインタラクティブになる(props の時のような再リクエストもない)。
</script>
<template>
<div>
+ <slot />
<div v-html="html" />
</div>
</template>
<template>
<MarkdownComponent>
<CountButton />
</MarkdownComponent>
</template>
制限4
3:44
(コンポーネントの?)ソースをリモートから取得 → v3.7 で解決済み。
ロードマップには interactive components within server components
とあるので、将来的には上記の「制限3」も解決するかも知れない
Next.js の App Router の流れもあるし、「データフェッチはサーバーコンポーネントでやる時代」に変わっていくのだろうなという印象。
参考: 当スクラップ記述時点のリポジトリ