2月11日にNext.js 8のリリースが公式ブログでアナウンスされました。
昨年9月のバージョン7のリリースから5ヶ月ぶりのメジャーアップデートですね。後方互換性を保ったアップデートとされています。
元記事で発表された新機能や改善点、変更点などをかいつまんでまとめてみました。
実際に自分で開発しているサービスをアップデートしてみた所感も少し書いています。
- 
新しく追加された機能
 2. サーバーレスに対応したビルド
 3. ビルド時の環境変数注入
 2. crossOrigin設定の追加
- 
改善点・変更点
 3. ビルド時のメモリ使用量の大幅な削減
 4. Prefetchのパフォーマンス向上
 5. 生成するHTMLのサイズ削減
 6. 開発用Webサーバの起動時間短縮
 7. Static Exportの速度向上
 8. Head要素の重複排除
 9. インラインJSの廃止
 10. 外部APIへAuthenticationするサンプルの公開
新しく追加された機能
サーバーレスに対応したビルド
アプリケーションをAWS Lambdaなどのサーバーレス環境にデプロイするための設定が追加されました。
pages/以下のファイル単位で単一の関数としてビルドされるようになっています。
有効化するには設定をこのようにします。
module.exports = {
    target: "serverless"
}
こうすると例えばpages/以下にindex.jsとabout.jsという2つのファイルがある場合、
pages/index.js => .next/serverless/pages/index.js
pages/about.js => .next/serverless/pages/about.js
こんなふうにビルドが行われます。
ファイルの中身はexpressでおなじみの引数にリクエストとレスポンスのオブジェクトを受けてページの内容を返す単一のrender()関数をexportする形になっています。
export function render(req: http.IncomingMessage, res: http.ServerResponse) => void
例えばデプロイ先のサーバーレス環境がNode.jsのhttpモジュールをサポートしている場合、以下のようにすることでレンダリングを行うことが出来ます。
const http = require("http");
const page = require("./.next/serverless/about.js");
const server = new http.Server((req, res) => page.render(req, res));
server.listen(3000, () => console.log("Listening on http://localhost:3000"))
良い感じですね!ちょっとLambdaにデプロイしたくなってきました。
ビルド時の環境変数注入
サーバサイドで動くWebアプリケーションを開発する時、実行時に環境変数を渡して参照することが多々あるかと思います。
Next.jsはサーバ・クライアント両方で動作するユニバーサルなフレームワークなので実行時に渡した環境変数は当然サーバサイドでしか参照できず、クライアントサイドと処理を分ける必要があるなど少し不便でした。
これまではこれに対するワークアラウンドとしてbabel-plugin-transform-defineやwebpack.DefinePluginを用いて、ビルド時に渡された環境変数をスクリプト内部に直接注入しサーバ・クライアント両方から参照可能にするということがよく行われてきました。
バージョン8ではNext.js自体にこの機能が取り込まれています。上記のモジュールを追加インストールすることなくデフォルトで設定ファイルに注入する環境変数を定義することが可能になりました。
module.exports = {
    env: {
        customKey: 'MyValue'
    }
}
このように書いておくとアプリケーションスクリプト内部のprocess.env.customKeyがビルド時に'MyValue'に置きかわり、サーバ・クライアント両者で実行時に参照することが出来ます。
自分もこれまではbabel-plugin-transform-defineを利用していました。デフォルトでこういうのがあると少しすっきりしていいですね。
crossOrigin設定の追加
Next.jsはビルドしたアプリケーションをブラウザで実行する時、page単位でjsを配信する仕組みになっています。
クライアントサイドルーティングは別pageへの遷移時にそのpageに対応したjsファイルを動的に生成した<script>タグを用いて注入することで実現しています。
今回のリリースではこの注入される<script>タグへcross-origin属性を付与する設定が追加されました。
これは単純に同一のドメインから全てのスクリプトを配信する場合は気にする必要のないものなのですが、スクリプトをCDNなど別のドメインから配信する時に効果的な設定です。
別ドメインからのスクリプトを読み込んだ際にcross-origin属性が付与されていないと
- エラー発生時にエラーの内容がコンソールに出力されず、全てScript Errorと出力される
- CORSのリクエストを行う時にCookieなどの認証情報が付与されない
といった不都合があります。
これを回避するために注入される<script>タグのcrossorigin属性に'anonymous'または'use-credentials'を指定しておく必要がありますが、今回それが設定ファイルから指定出来るようになりました。
module.exports = {
    crossOrigin: 'anonymous'
}
ちなみにNext.jsにはスクリプトの読み込み先をCDNなど別のドメインに変更するassetPrefixという設定があります。
多分これとセットで使うことが想定されていると思います。
改善点・変更点
ビルド時のメモリ使用量の大幅な削減
速度の低下など全く無しにアプリケーションのビルドに必要なメモリを従来の16分の1に削減し、なおかつメモリの解放も早くなったようです。
これにより大規模なアプリのビルドが不安定でクラッシュしたりすることもなくなるでしょう…とのこと。すごいですね。
これはNext.jsの改善というよりはwebpack自体のパフォーマンスが向上したためのようで、そのためにwebpackにめっちゃcontributionしたって書いてありました。
どのように実現したのか詳しくはここには書かないけどそのうちまとめるからブログ見てね、とのことです。
Prefetchのパフォーマンス向上
クライアントサイドルーティングを簡単に実現するLinkコンポーネントのprefetch属性に関する変更です。
これまではページ内にprefetchが指定されたLinkがある場合、遷移先のURLで使うスクリプトを<script>タグを使って注入することで遷移前の先読みを行なっていました。
しかしこれではスクリプトの読み込みが終わるまでページの読み込みも完了しなくなってしまいブラウザに不必要な待ち時間を与えてしまいます。
今回の変更では<script>タグの注入による先読みを廃止し代わりに<link rel="preload">を用いることでページの読み込みが完了してはじめてスクリプトの先読みが始まるようになりました。
加えてブラウザのnavigator.connection.saveDataの値を参照して自動的に先読みが無効になるようになったようです。
<link rel="preload">を用いた実装だと先読みの振る舞いがブラウザ依存になるので以前の強制的にスクリプトを読み込ませる方式と比べるとお行儀が良くなった感がありますね。
ちなみにこのprefetchを有効にしてみたらChromeでこんなWarningが出てしまいました。
preloadしてから3秒以内に当該のスクリプトを利用しないと不要な先読みだと捉えられて怒られてしまうようです。
The resource was preloaded using link preload but not used within a few seconds from the window's load event. Please make sure it wasn't preloaded for nothing.
next/routerの提供するprefetch()を利用しても同じことが出来るので、自分はリンク要素にマウスカーソルがのった時点でprefetchが行われるようにしています。
import Router from 'next/router'
import Link from 'next/link'
export default props => (
    <Link href='/about'>
        <a onMouseEnter={() => Router.prefetch('/about')}>About</a>
    </Link>
)
このPrefetchの挙動を確認してみたい方は僕が運営しているtechbooksという技術書籍のレビュー・ランキングサイトで実際に実装されているのでよかったら見てみて下さい。
生成するHTMLのサイズ削減
サーバサイドでレンダリングするhtmlのサイズが1.50KB → 1.16KBと23%削減されました。
エラーページ表示用のスクリプトを初期描画時に含めないようにしたことと、後述するインラインスクリプトの廃止の影響によるものとのことです。
開発時のオンデマンドコンパイルの改善
Next.jsは開発サーバの起動時に全てのスクリプトをコンパイルせず、どこかのページにリクエストがあってはじめてそのページに関するスクリプトをコンパイルして画面を描画することで開発時のパフォーマンスを向上させています。
また最初にページをコンパイルした時点でその結果をキャッシュとしてメモリに保持し、25秒間そのページにリクエストがなければそれを破棄することによって不要なメモリの解放もよしなにやってくれています。
これまでは現在開いているページのキャッシュを破棄しないよう滞在検知のために5秒おきにwindow.fetchによるポーリングを行なっていましたが、これが今回WebSocketによる実装に変更されました。
従来の方法だと5秒毎に開発者ツールのNetworkタブにポーリングの結果がどんどん表示されてしまって不便だから、というのが理由みたいです。
これによって開発サーバがListenするポートがWebSocketサーバ用に1つ追加されました。
(自分はDocker環境で開発サーバを起動していた上にこのことを知らず少しはまりました)
デフォルトではWebSocketサーバは適当に空いているポートを探してListenするみたいですが任意のポートに固定する設定も追加されています。
間に何らかのプロキシをかませていてWebSocketサーバにListenして欲しいポートとブラウザにリクエストして欲しいポート/パスが異なる場合はそれ用の設定も出来るみたいです。
module.exports = {
    onDemandEntries: {
        websocketPort: 3001,
        websocketProxyPort: 7001,
        websocketProxyPath: '/hmr'
    }
}
開発用Webサーバの起動時間短縮
開発サーバの起動時、これまでは
初期リソースのコンパイル → Webサーバ起動、ポートのListen
という流れだったのでnextコマンド実行直後にブラウザでアクセスしてもThis site can’t be reachedなどのエラーが表示されてしまっていました。
バージョン8ではこれが逆になって
Webサーバ起動、ポートのListen → 初期リソースのコンパイル
になりました。
開発用Webサーバ自体はnextコマンド実行直後に立ち上がり、すぐにアクセスしてもエラーが表示されないようになりました。
ちゃんとコンパイルの完了まで読み込み待ちになるみたいです。
Static Exportの速度向上
サーバサイドのレンダリング結果を静的ファイルとして出力するexportコマンドがマルチコアに対応しました。
4コアのMacBookで試したところ25ページ/秒 → 75ページ/秒と3倍も高速になったとのことです。
Static Exportを利用してブログサイトを構築している場合などは出力するページ数が多くなることが想定されるのでこれが速くなるのは良いですね。
Head要素の重複排除
どんなコンポーネントからでもnext/headを用いるとページの<head>内に任意のタグ/コンポーネントを注入することが出来ますが、これまでは例えば<title>など重複して追加されるのではなく上書きをしてほしい要素の重複をコントロールする方法がありませんでした。
今回、<Head>内の要素に付与する任意のkey属性でこれをコントロールすることが出来るようになりました。
以下のコードはこれまでなら<head>内に<meta name="viewport" ... />の要素が2つ重複して注入されてしまっていましたが、今回の変更では同一のkeyを持つ要素は上書きされるようになっています。
import Head from 'next/head'
export default function IndexPage() {
    return <>
        <Head>
            <title>My page title</title>
            <meta name="viewport" content="initial-scale=1.0, width=device-width" key="viewport" />
        </Head>
        <Head>
            <meta name="viewport" content="initial-scale=1.2, width=device-width" key="viewport" />
        </Head>
    </>
}
インラインJSの廃止
これまではページ内にインラインjsとして埋め込むことでサーバからクライアントへデータの受け渡しを行っていましたが、<script type="application/json">を利用した埋め込みに変更されました。
これによりNext.jsによるページへのインラインJS埋め込みは完全に無くなったとのことです。
外部APIへAuthenticationするサンプルの公開
これはNext.js自体のアップデートというわけではないですが、ユーザが外部APIに対してCookieを利用した認証を行うケースのサンプルコードが公開されました。
https://github.com/zeit/next.js/tree/canary/examples/with-cookie-auth
サイトにSNS連携やOAuth認証の機能を追加する場合などもこれに当てはまりますね。
どうやって実現すればいいのか質問が多かったそうです。
- サーバサイドレンダリング時にもブラウザが送信してきたCookieと一緒にAPIへリクエストを行う
- Proxyサーバを使ってCORS関係なくリクエスト出来るようにする
この2つを実装すればクライアントサイド・サーバサイドで振る舞いを気にしなくて良くなるよ、との回答を示した形のサンプルコードになっています。
まとめ
たくさんの改善が含まれたメジャーアップデートでした。
実際に使ってみたところ確かに体感的にも開発用サーバのパフォーマンスがすごく向上していると感じました。
ビルド後のアプリケーションのパフォーマンスはもちろん、こうしたDXの改善にも意欲的なのはとても嬉しいですね。
筆者はTwitterで日々技術情報の投稿をしています。是非フォローしてもらえると嬉しいです。