nuxt.js による UX/DX フレンドリーな Web サービス開発

nuxt.js logo

初めまして。ザ・シードオンライン開発担当の山岸です。

バーチャルキャスト社が提供している 3D モデル流通プラットフォーム ザ・シードオンライン では、フロントエンド技術に nuxt.js を採用しています。

nuxt.js を一行で説明すると、「 Vue.js エコシステムをベースに、 UX/DX フレンドリーな Web サービスを作れるフレームワーク」 です。

つまり、そのサービスを利用するエンドユーザの体験もよく、そのサービスを開発するエンジニアユーザの体験もよく、という利用者・開発者どちらにもメリットのあるフレームワークとなっています。

どのような部分でフレンドリーなのか、紹介します。

ユーザーエクスペリエンス・フレンドリー

実際に Web サービスにアクセスするエンドユーザへのサポートです。

パフォーマンス

Web サービスの指標として最も注目されるのは、やはりパフォーマンスです。

nuxt.js では、ブラウザの最新技術や Vue.js の豊富なエコシステムをふんだんに活用して、ユーザにコンテンツを見せられるまでの時間、画面更新の時間を最短に近づけています。

クリティカルパス最適化

progressive rendering

画像参照元: クリティカル レンダリング パス  |  Web  |  Google Developers

パフォーマンスが最もわかりやすく現れるのは、 「ページに到達した瞬間」 の体験です。

検索結果や紹介ブログ記事など、どこかのページのリンクから、ブックマークから、または SNS から、ユーザは Web サービスにたどり着きます。

ユーザが到達してからコンテンツが表示されるまで、具体的に言えば「今見ている画面に収まる部分が描画されるまで」の時間がキーになります。

その「今見ている画面」のことを クリティカル(レンダリング)パス(CSS), Abobe the fold などと言われますが、そこを最適化するための機能が内蔵されています。

  • サーバサイドレンダリングモードを用いて、事前にサーバ側で動的なパーツの HTML を生成する
  • クリティカルパスを推測し、その部分で利用されている CSS を HTML 上に展開(インライン化)する
  • <script> タグによるレンダリングブロックを避ける
  • クリティカルパスでない下の部分の画像などを後から読み込むようにする

など、様々な施策を用いて最適化を行っています。これらほとんどは エンジニアが頑張って最適化せずとも、自動で提供されます

どれくらいの最適化が行われているのかは、 nuxt.js 自身の静的生成されたページ を見れば一目瞭然ではないかと思います。

事前のローディング画面や構成が中途半端な状態はなく、一瞬で画面が切り替わるように表示されたのではないでしょうか。

次のページのプリロード

クリティカルパスの次は、 「ページを遷移した時」 の体験です。

つまり、リンクをクリックしてサブページに移行するタイミングです。

  • HTTP/2 プッシュ機能 を用いて、ページを移動する前から次のページ(=このページにリンクがついているページ)に必要なアセット(js, css)を事前に取得する
  • ページ移動は HTML を取得する形式ではなく、 vue-router を利用して差分となるアセットだけを取得して移動する

これら機能も自動で提供され、 ただ Vue を書くだけですでに UX フレンドリー になっているのです。ここが nuxt.js の非常に強いメリットとなっています。

サーバサイドレンダリングによる端末負荷軽減

通常の Vue.js だけでは、最初は空の HTML が提供され、 JavaScript によって構築されて最終的に見た目が表示されるようになります。

つまり HTML の組み立てを端末で行うため、スペックの低い端末では表示まで時間がかかってしまいます。

nuxt.js は様々な手法でサービスを提供出来ますが、 node.js HTTP サーバを用いたサーバサイドレンダリングモードを使えば、スペックの低い端末でページを表示した場合でも、 HTML は組みあがったものがすでに提供されるので軽い負荷で表示が可能です。

SNS 共有時のメタ情報

各種ページの URL を共有する時、 SNS 上では URL だけでなくページタイトル・画像・説明文などを載せることが可能になっています。

og meta

これは オープン・グラフ・プロトコル などを用いて、 <meta> タグに記述することで実現可能です。

vue-meta を利用することで、手軽に <meta> タグを調整することが可能です。

モダンビルドによる最新ブラウザへの最適化

vue-cli-service に導入されている「モダン」ビルドを利用すれば、最新のブラウザでより最適な JavaScript ローディングを行うことが出来ます。

これについては 別途 Qiita に記事にしています

サービスワーカーによるオフラインデータキャッシュ

Web サービスでは、ページを読み込むたびにサーバにアクセスして、新しい情報を取得します。

しかし、その読み込みごとに内容が変わるものもあれば、変わらない画像などのアセットもたくさんあります。

それらの変わらないアセットを毎回サーバから取得してくるのは無駄が生じます。

Web サービスは様々な機能を使ってそれらの内容が同じアセットをキャッシュして、毎回通信しないようにすることで高速化を実現しています。

nuxt.js では、追加モジュールとして pwa workbox というものがあり、これを利用してブラウザ内キャッシュをフル活用し、 オフライン状態であっても ページを出来るだけ閲覧出来るような構成が可能です。

これは Google の Workbox というライブラリを用いてアセットをキャッシュし、一度取得したアセットは再度通信しなくても利用出来るようにしています。

通信をしないことで、より高速な描画を実現しています。

デベロッパエクスペリエンス・フレンドリー

nuxt.js では、エンドユーザに対するアプローチの最適化だけでなく、 nuxt.js を利用するエンジニアに対しても使いやすいような Developer Experience の最適化を行っています。

利用するフレームワークやライブラリの扱いやすさは、開発効率に直結するものなので重要です。

圧倒的に手軽な導入

Vue.js は Vue CLI を利用してインタラクティブにプロジェクトを開始することができます。

nuxt.js でも同様に $ yarn create nuxt-app <my-project> コマンドを叩くだけでインタラクティブにテンプレートを選択し、 $ yarn dev で開発用のサーバがローカルに立ち上がります。面倒な初期設定は一切不要です。

また、テンプレートなしの最小構成はなんと package.jsonpages/index.vueたったの 2 ファイル で Hello World が出来る Well-configured なフレームワークです。

煩雑なビルド・バンドル処理をゼロコンフィグで

現代の JavaScript 界隈では、「最新機能を使いたいが古いブラウザにも対応したい」「TypeScript で書きたい」などの需要が高く、スクリプト言語にも関わらずビルド処理を必要とする場合が多いです。本番向けにファイルを minify することもあるでしょう。

また、アセット間の依存関係を解決し、各ページで必要なアセットのみをバンドルして 1 つのファイルにするなどの処理も頻繁に行われています。

これらの処理は babel, webpackrollup などといったツールを利用して行われるのですが、これらツールをまとめて設定するのが大変な作業だったりします。

nuxt.js では、デフォルトでビルド・バンドル処理を自動的に行ってくれるため、 設定を一切変えずに 達成することが可能です。

豊富で設定可能なビルド機能

ビルドが設定不要であるということは、初期設定がすでに最適化されていて、ある程度の要件を最初から満たせるということです。

現状ではデフォルトで以下のビルドが可能となっています。

  • babel -> @nuxt/babel-preset-app で自動的にいい感じの babel ビルドを行う
  • file -> テキストファイルを JavaScript 内で import 読み込み可能にする
  • font, img -> 小さい外部ファイルを base64 でインライン展開する
  • less, sass, scss, stylus, postcss などの CSS プリプロセッサ -> 自動でビルド
  • TypeScript -> @nuxt/typescript で自動ビルド可能
  • vue -> もちろん vue のビルドも

先ほど「ビルドは設定不要で可能」と言いましたが、作るプロダクトのニーズに合わせてビルドを調整したい場合が必ず出てきます。

その場合も、 nuxt.config.js ファイルでかなり自由に拡張することが可能です。

また、 module ライブラリとして外部からビルド設定を変更出来るように拡張することも出来ます。

3 パターンのサーブ方法

nuxt.js は、作成したプロダクトをユーザに提供するのに 3 つの方法を用意しています。それぞれにメリット・デメリットがあります。

3 つ用意することで、間口を広く、様々な要件を満たせるように作られています。

1. サーバサイドレンダリング

node.js HTTP サーバを利用したサーバサイドレンダリング (Universal) です。

これは、ユーザからリクエストが来た時に事前にサーバ側で vue-server-renderer を用いて Vue をレンダリングし、その結果生成された HTML を返す手法です。

  • メリット
    • スペックの低い端末でも高速に描画出来る
    • 動的なパスを持つページ(/users/5 など)のメタ情報を適切に取得出来る
  • デメリット
    • node.js サーバの保守・運用コストがかかる

ザ・シードオンラインは、アイテムを SNS に共有したりする関係上、二つ目のメリットが必須になったためこの手法を採用しています。

続く 2 つの手法では、 /users/5 ページを SNS に共有しても、 404 になるかトップページと同じ情報しか表示することが出来ません。

2. 静的生成

事前に描画に必要なファイルをすべて生成し、それを CDN 等に載せるだけの超簡単な手法です。

$ nuxt generate コマンドでファイル群が生成されるので、それを Amazon S3 や GitHub Pages, Netlify などにアップロードして終わりです。

  • メリット
    • 運用コストがほぼゼロ (静的なファイルを publish するだけなので)
    • CDN に置けば全世界で高速にサーブすることが可能
    • ビルドも事前に終えているのでメタ情報も載るし描画も早い
  • デメリット
    • 動的なパスを持つページのメタ情報が適切に取得出来ない

ドキュメントサイトやポートフォリオサイトなど、動的にページを増減することのない Web サービスに有効です。

3. Single Page Application として生成

事前に シングルページアプリケーション としてビルドすることで、サーバに依存しないサービスファイルを生成します。

$ nuxt build --spa コマンドで生成されたファイルをアップロードするだけです。

静的生成と異なる点は、 「事前にコンテンツのレンダリングを行わない」 所です。

静的生成では全てのコンテンツを 事前に レンダリングする必要があるため、外部サービスへ依存したり、特定の条件でしか描画出来ないコンテンツがある場合にそれを描画することが出来ません。

SPA モードで生成されたファイルには、静的生成と同様のメタ情報やアセットの URL などは埋め込まれていますが、 Vue で作るコンテンツ部分は描画されていません。

これで例えば「サーバサイドレンダリングが出来ないが、ログインして取得したユーザ情報をコンテンツに反映させたい」といった需要を満たすことができます。

  • メリット
    • 認証が必要なページなどでも静的なファイル生成でカバー出来る
    • 運用コストがほぼゼロ
  • デメリット
    • ブラウザ側でレンダリングを行うため、少し低速
    • 動的なパスを持つページのメタ情報が適切に取得出来ない

レンダリング速度を下げてでも node.js サーバを運用したくない場合、この生成手法が役に立つでしょう。

ファイルベースルーティング

どのパスがどの Vue を表示するのかは重要な話題です。

サーバサイドでは routes.php などに手動でパスとコントローラの紐づけを行うことが多いですが、 nuxt.js はシンプルに /pages/app/user.vue ファイルは /app/user というファイルベースのルーティングを実現しています。

アンダースコアから始まる名前で任意のパス変数を受けることも可能です。

これで、ディレクトリを見るだけで「このファイルはこのパスの時に描画されるな」と一目でわかるようになっています。

asyncData による描画前のデータ取得

サーバサイドレンダリングを行うためには、 Vue コンポーネントを描画する 前に 必要なデータを収集する必要があります。

nuxt.js では、 asyncData という Vue コンポーネントメソッドを定義することで、 data に非同期で情報を注入することが出来ます。

戻り値は通常の data メソッドと同様にコンポーネントに注入され、テンプレートや他メソッドで利用可能になります。

また、サーバサイドレンダリング時だけでなく、ページを移動した際にもブラウザ側でこのメソッドが呼ばれるため、どこでレンダリングするかに依存しない実装にする必要があります。

axios module は、 node.js 上でもブラウザ上でも同じインターフェースで HTTP リクエストを送れるライブラリです。公式ではこのライブラリを使って HTTP リクエストを送ることが推奨されています。

ザ・シードオンラインでは、先述した workbox module を最大限活用するため、 axios ではなくブラウザ標準の fetch API を利用した実装を行っています。

プラグインによるランタイムの拡張

Vue.js は独自の プラグイン機能 で、 Vue インスタンスをカスタマイズすることが出来ます。

nuxt.js でもそれらの既存プラグインを利用出来るように、プラグインを定義することが可能です。

これらはサーバ・クライアント共に Vue インスタンスを初期化する前に実行され、 Vue コンポーネントの中で利用することが出来るようになります。

また、 inject 機能を用いることで、 asyncDataVuex Store 内でも同じように利用出来るプラグインを定義することが可能です。

モジュールによるフレームワークの拡張

nuxt.js 単体でも豊富な機能を持ち合わせていますが、もちろんすべての要件を満たせるわけではありません。

そこで、コア機能の拡張を行うための モジュール機能 が内蔵されています。

モジュールを用いることで、フレームワークの拡張機能を単体のライブラリとして提供出来るようになります。

awesome-nuxt を見てわかる通り、 36 の公式モジュールと、 50 以上のコミュニティモジュールが公開されています。

nuxt.js 利用者は、これらたくさんのモジュールの中から利用したいものを選び、 $ yarn add module-name して nuxt.config.js の中にオプションを記述するだけで、 手軽に拡張機能を利用できるようになります

TypeScript サポート

nuxt.js はこれまで、「公式として TypeScript をサポートする予定はない」としていましたが、 kevinmarrec さんがコアメンバーに参加したことで、一気にそのサポートの幅を広げていきました。

v2.4 から nuxt-ts パッケージを利用して、すべての場所で TypeScript を利用出来るようになったり(当時 Experimental でした)、 v2.4 から @nuxt/typescript パッケージとして提供されるようになったことで、公式の型定義や自動補完、ランタイムでの TypeScript サポートを得られるようになりました。

examples/typescript を見れば、どのように TypeScript をプロジェクトに導入できるかがわかると思います。

豊富なサンプルパッケージ

先ほど紹介した typescript もそうですが、公式リポジトリの examples ディレクトリには、様々な nuxt.js の利用方法が紹介されています。

各種公式モジュールを導入してみたもの、少し細かい設定の仕方など、実践的な構成にするにあたって大変参考になるものが並んでいます。

いろんなサイトやサンプルリポジトリを巡って構成を考えることもなく、公式のサンプルからすぐに自分のプロダクトにあった構成を作っていけるので、スタートアップの効率も非常に高いものとなっています。


このように、 nuxt.js は 「エンドユーザ」にも「エンジニア」にも 快適な体験を提供するフレームワークとなっています。

今回ザ・シードオンラインを作る際にも、(細かいハマリどころは結構ありましたが)気持ちよく導入してコードを書いていくことが出来ました。

現在も非常に活発なコミュニティで開発が進められていて、また Vue.js も v3 に新しくなることで、大きな進歩があると思います。今後の成長に大きく期待できる状態です。

nuxt.js はいいぞ。