SMARTCAMP Engineer Blog

スマートキャンプ株式会社(SMARTCAMP Co., Ltd.)のエンジニアブログです。業務で取り入れた新しい技術や試行錯誤を知見として共有していきます。

Nuxt3のNitroでどれくらい早くなったか検証してみた

初めに

こんにちは、スマートキャンプ エンジニアの林です。 前回は私の自己紹介記事でしたが、今回は技術的な話をしたいと思います。

現在、Webフロントエンドを書くならNuxt.jsかNext.jsが選択される事が多いですが、皆さんはどちらがお好きですか? 私はVue.jsを使用したフロントエンド開発の経験が長いので、Nuxt.jsに親和性が高く、Next.js(React.js)の時代とされる今でも積極的にVue.jsを追いかけています。

そこで今回は旬なフレームワークであるNuxt3について書いていこうと思います。

TL;DR

  • Nuxt3のNitroによってどれくらい早くなったか検証をしてみた。
  • サーバー側からのAPIへのレスポンス速度がNuxt2に比べて良くなったことが観測できた。
  • クライアント側からのAPIへのレスポンス速度の上昇は観測できなかった。

Nuxt3とは

今年の12月にNuxt3のパブリックベータがリリースされたことはご存じの方も多いかと思いますが、あらためてNuxt3についておさらいしたいと思います。 公式サイトを覗いてみるとこんな記述がありました。(DeepLを使用して日本語になおしてみます)

Vue 3で次のアプリケーションを構築し、ハイブリッドレンダリング、パワフルなデータフェッチ、新機能を体験してください。Nuxt 3は、Web開発をシンプルかつパワフルにするオープンソースのフレームワークです。

Nuxt3はvue3で書かれていてさまざまな新機能が追加されていることがわかります。 具体的にどんな機能が追加されているかに関しては他の記事に譲ります。

そのまま、少し下にスクロールして読みすすめると、Nitro Engineという新しいサーバーエンジンついて以下のように言及されていました。

You can deploy this output on any system supporting JavaScript, from Node.js, Serverless, Workers, Edge-side rendering or purely static. (中略) The foundation of the Nitro server is rollup and h3 : a minimal http framework built for high performance and portability.

NitroはNode.jsだけじゃなくてServiceWorkerやサーバーレス環境でも動作して、さらにh3という軽量のhttpサーバーが使われているようですね。

H3

「ふーん、Nuxt速くなったのかー!すげー!」

と終わらせてしまいたいところなのですが、 今回はNuxt3で新たに導入されたNitroエンジンで作ったAPIのパフォーマンスをNuxt2で作ったAPIと比較しながら調査していきます。 このような計測は初心者なので、気になるところなどありましたらコメントお待ちしております。

Nitroとは

検証に入る前に、Nuxt3に初めて搭載されたNitroについて紹介していきたいと思います。 Nitroは以下の利点があると公式ドキュメントでは記載されています。

  • Cross-platform support for Node.js, Browsers, service-workers and more
  • Serverless support out-of-the-box
  • API routes support
  • Automatic code-splitting and async-loaded chunks
  • Hybrid mode for static + serverless sites
  • Development server with hot module reloading

Node.js環境だけじゃなく、サービスワーカやブラウザでも動作したり、ホットリロードで快適に開発できることや、内部にh3が組み込まれており、関数の返り値をオブジェクトにするだけで、レスポンスをJSON形式にできたりします。また、Promiseもサポートしているので、非同期処理も簡単に扱うことができるようです。

Nuxt3 Server Engine

それでは、Nuxt2とNuxt3両者でどれだけコードの記述量を比較するために、 api/helloにアクセスが来たら'Hello World'と返却するAPIを作成してみます。

Nuxt2

Nuxt2ではserverMiddlewareを使用することでAPIサーバーを作成できます。またserverMiddlewareは他のNodejsフレームワークを使用して、拡張可能なので、今回は便宜上Expressを使用します。

import express from "express";
const app = express();

app.get("/hello", function (req, res) {
  res.send("Hello World");
});

export default {
  path: "/api",
  handler: app,
};

Expressのインスタンス作成後、getを使用して'/hello'のルートを作成しています。exportする際にpathに'/api'、handlerにappを指定することで、/api/*にアクセスがくると、関数が実行されるようになるAPIを作成できます。こうしてみると結構記述量が多い印象ですね。

Nuxt3

一方Nuxt3では'/server/api'ディレクトリに任意のファイル名でファイルを設置すると、それ自体がAPIのパスになります。 今回だとのケースだと/server/api/hello.tsとするだけでAPIルートを作成できます。便利ですね。

export default (req, res) => {
  return "Hello World"
};

Nuxt2の実装とは大きく違い、ただ関数を作って文字列を返すだけでAPIが作成できてしまいます。内部でjson形式のレスポンスに変換してくれているので明示的に記述する必要がなく非常にシンプルですね!

ちなみに自動でJSONに変換している部分はH3を使用して実現されていますが、以下のような実装になっていました。 middlewareで渡された関数の返り値のtypeがstringだった場合は、html形式のレスポンスになり、それ以外はJSON形式に自動で変換される処理になるようですね。 https://github.com/unjs/h3/blob/main/src/app.ts#L92

  const val = await layer.handle(req, res)
  if (res.writableEnded) {
    return
  }
  const type = typeof val
  if (type === 'string') {
     return send(res, val, MIMES.html)
  } else if (type === 'object' && val !== undefined) {
  // Return 'false' and 'null' values as JSON strings
  if (val && val.buffer) {
    return send(res, val)
  } else if (val instanceof Error) {
     throw createError(val)
  } else {
    return send(res, JSON.stringify(val, null, spacing), MIMES.  json)
  }
}

他にもuseQuery、useBodyなどを使用してリクエストの中身を簡単に取り出すことができます。

import { useBody, useCookies, useQuery } from 'h3'

export default async (req, res) => {
  const query = await useQuery(req)
  const body = await useBody(req) // only for POST request
  const cookies = useCookies(req)
  return { query, body, cookies }
}

また、ホットリロードによってコードを変更すると高速でビルドが走らせる事ができるため、 爆速でAPIサーバーを作成できます。

次は作成したこれらのAPIを使ってNuxt3とNuxt2を比較してパフォーマンスがどれだけ変わったか見ていきます。

パフォーマンス計測

それでは、計測に使用するコードを書いていきます。

計測に使用するコード

async function timer(promise) {
  const start = Date.now();
  await promise;
  const end = Date.now();
  const time = end - start;
  console.log(`time: ${time}ms`);
  return time;
}
<template>
  <div>averageTime: {{ averageTime }}ms</div>
</template>

まずはtimerという関数を作成しました。引数にPromise Functionを渡して実行時間を取得できます。

それではpages/以下にコンポーネントを作成し、Nuxt2、Nuxt3で10回APIコールを行った結果の平均時間を取れるコードを書いていきます。

Nuxt2

<template>
  <div>
    <div>averageTime: {{ averageTime }}ms</div>
  </div>
</template>

<script lang="ts">
export default {
  name: "IndexPage",
  async asyncData(ctx) {
    const result = await Promise.all(
      [...Array(10)].map((_, i) =>
        timer(ctx.$axios.get("http:/localhost:3000/api/hello"))
      )
    );
    return {
      averageTime: result.reduce((prev, current) => prev + current) / 10,
    };
  },
};
</script>

Nuxt2ではasyncDataを使用することで、Nuxtインスタンスが生成される前にAPIコールを行なうことができます。Promise.allを使用することでAPIコールを並列で行った結果を取得します。

Nuxt2 Data Fetching

Nuxt3

<script lang="ts">
export default defineComponent({
  async setup() {
    const result = await Promise.all(
      [...Array(10)].map(async (_, i) => timer(useFetch("/api/hello", { key: `${i}` })))
    );
    return {
      averageTime: result.reduce((prev, current) => prev + current) / 10,
    };
  },
});
</script>

<template>
  <div>averageTime: {{ averageTime }}ms</div>
</template>

Nuxt3ではasyncDataの代わりにuseFetchまたはuseAsyncDataを使用することで、Nuxtインスタンスが生成される前にAPIコールを行なうことができます。 今回は内部でuseAyncDataを使用しているuseFetchを使用することにします。

Nuxt3 Data Fetching

これで準備が整ったので計測していきます。

検証

サーバー側とクライアント側の両方の動作を確認したいので、計測は2種類行います。

  • 画面描画時(サーバー側)
  • 画面描画後(クライアント側)

Nuxt Lifecycle

結果はconsoleに表示されるので、あらかじめGroup similar messages in consoleのチェックを外しておきます。こうすることで1回と2回目のレスポンス結果が同一でも別々に表示してくれて見やすくなります。

上記の設定をしたことで、同一結果がグループ化されずに別々に表示されています。

画面描画時(サーバー側)

ブラウザがリロードされたタイミングで処理が行われるので、5回ほどリロードしながら計測します。

Nuxt2で計測
1回目: averageTime: 16.2ms
2回目: averageTime: 14ms
3回目: averageTime: 15.8ms
4回目: averageTime: 13.5ms
5回目: averageTime: 12.8ms

5回実行した平均は14.46msでした。 ローカル環境で実行しているので当然ではありますが、普通に速いですね。

Nuxt3で計測

続いてNuxt3の結果をみていきます。

1回目: averageTime: 3.2ms
2回目: averageTime: 4.7ms
3回目: averageTime: 2.6ms
4回目: averageTime: 4.1ms
5回目: averageTime: 3.6ms

5回実行した平均は3.64msでした。 Nuxt2も速かったですが、更に速い結果がでました。

Nuxt3はNuxt2より約3.8倍も速い結果となり、パフォーマンスの大幅に向上が確認できました。 次は画面描画後(クライアント側)での動作確認も行っていきます。

画面描画後(クライアント側)

画面上に押されたらAPIコールを実行するボタンを設置して確認していきます。

// nuxt3
const handleFetch = async () => {
  return await timer(useFetch("/api/hello"));
};
// nuxt2
methods: {
 handleFetch: async () => {
   return await timer(fetch("/api/hello"));
 },
}

<template>
 <button @click="handleFetch">再取得</button>
</template>

5回ほどポチポチ実行します。

Nuxt2で計測

平均7.9msでした。 サーバー側の時より速いですね・・・。

Nuxt3で計測

平均10.9msでした。

ほとんど結果は同一で、クライアント側ではほとんどパフォーマンスの向上してないという結果になりました。

結果

サーバー側はNuxt3がNuxt2に比べ約3.8倍も良い結果でしたが、クライアント側ではほとんど差は見られないという結果となったことから、nitroによって作られたAPIサーバー自体が速くなったわけではなさそうでした。

ただ、サーバー側のパフォーマンスは向上していそうだったので、これだけでもNuxt3を使う価値はありそうですね。

こういった計測はなかなか慣れておらず、至らない点が多いかと思います。もしご指摘などありましたらコメントをお待ちしております。

感想

「Nuxt3にNitroが搭載されてどれだけ速くなったの?」と疑問に思ったことが、今回の記事を書くきっかけとなったのですが、Nuxt3自体のパフォーマンス向上もさることながら、開発体験が非常に良かったのが驚きでした。

例えばnpm run devで開発サーバーを起動する時間も、Nuxt2だと約4秒かかっていましたが、 Nuxt3では約0.6秒で起動でき、とても気持ちよく開発できました。

  • Nuxt2 nuxt2

  • Nuxt3 nuxt3

まとめ

本記事では、Nuxt3とNuxt2で作成したAPIサーバーのパフォーマンスを比較した結果をご紹介しました。

Nuxt2に比べ高速化・最適化されている部分が多いNuxt3でどんどん開発していきたいですね。

最後までお読みいただきありがとうございました!

宣伝

イベント情報

2/28に弊社開発チームのリモートワークコミュニケーションについてお話しするイベントを開催します! ご興味ありましたら以下のリンクから内容や参加方法についての詳細をご覧ください!

smartcamp.connpass.com