Nextjs+TypescriptでHTMLをStatic Exportしたときにcssをinlineで出力する方法

# 要約

- 2021年1月現在、Nextjsでstatic exportするとcssがインライン( `<style>` タグにベタ書き)にならない。
- cssを `<link>` タグでロードすると同期的にロードされてしまい、ページのレンダリングが遅くなるのでinlineにしたい(inlineだと非同期ロードなのでレンダリングまでの時間が早い)
- そこで[参考にしたgithubのissue](https://github.com/vercel/next-plugins/issues/238)にあるコードを元に（というかまんまコピー）`_document.tsx` を書いた。
- この記事では一応何をやっているか自分のための備忘録として書いておく
- 使っているNext.jsのバージョン: `10.0.5`

```tsx
import Document, { Head, Main, NextScript } from "next/document" 
import fs from "fs"
import path from "path"

declare type DocumentFiles = {
    sharedFiles: readonly string[];
    pageFiles: readonly string[];
    allFiles: readonly string[];
};

class InlineStylesHead extends Head {
    getCssLinks({ allFiles }: DocumentFiles) {
        const { assetPrefix } = this.context
        if (!allFiles || allFiles.length === 0) return null
    
        return allFiles
            .filter((file: any) => /\.css$/.test(file))
            .map((file: any) => (
                <style
                    key={file}
                    nonce={this.props.nonce}
                    data-href={`${assetPrefix}/_next/${file}`}
                    dangerouslySetInnerHTML={{
                        __html: fs.readFileSync(path.join(process.cwd(), '.next', file), 'utf-8'),
                    }}
                />
            ))
    }
}

export default class CustomDocument extends Document {
  render() {
    return (
      <html>
        <InlineStylesHead />
        <body>
          <Main />
          <NextScript />
        </body>
      </html>
    )
  }
}
```

# やっていること

NextJSの `Head` コンポーネントを継承した `InlineStylesHead` というのを定義している。NextJSの `Head` コンポーネントには `getCssLinks` という関数があり、ここでnextjsでビルドされたcssを呼び出すための `<link>` タグを生成している。 `InlineStylesHead` では同じようなことをしているが、 `<style>` タグでビルドされたcssを直接書いてしまっている。[vercel/next.jsにある_document.tsxのコード](https://github.com/vercel/next.js/blob/canary/packages/next/pages/_document.tsx#L155) をみると

```tsx

getCssLinks(files: DocumentFiles): JSX.Element[] | null {
  //...長いので省略    
}
```

と書いてあるので、getCssLinksという関数には `DocumentFiles` という型の `files` という引数を渡される。が、 `DocumentFiles` という型はexportされていないので自身で定義している。定義自体は[ここにある](https://github.com/vercel/next.js/blob/canary/packages/next/pages/_document.tsx#L40)ので

```tsx
declare type DocumentFiles = {
    sharedFiles: readonly string[];
    pageFiles: readonly string[];
    allFiles: readonly string[];
};
```

を `_document.tsx` で定義している。さて、これでビルドされたCSSのファイル一覧を取得できるようになったので、あとは `Head` クラス自体が持っているアセットのパスと結合し、ファイルを読み込んで `<style>` タグとして出力する。

```tsx
getCssLinks({ allFiles }: DocumentFiles) {
    const { assetPrefix } = this.context
    if (!allFiles || allFiles.length === 0) return null

    return allFiles
        .filter((file: any) => /\.css$/.test(file))
        .map((file: any) => (
            <style
                key={file}
                nonce={this.props.nonce}
                data-href={`${assetPrefix}/_next/${file}`}
                dangerouslySetInnerHTML={{
                    __html: fs.readFileSync(path.join(process.cwd(), '.next', file), 'utf-8'),
                }}
            />
        ))
}
```

これを定義したコンポーネントを普段使う `<Head>` (これはNextJSのやつ) の代わりに使うとinlineで出力される。

# まとめ

- NextJSの`Head` コンポーネントではデフォルトでcssをinlineで出力しない
- なので自分でそれっぽいコンポーネントを書く必要がある。
- [参考issue](https://github.com/vercel/next-plugins/issues/238)にあったコードを元に作った

## ほんとにinline化する必要ある？

- [Google](https://developers.google.com/speed/docs/insights/OptimizeCSSDelivery?hl=ja)によれば「ちっちゃいcssならinlineでいいけど、大きいcssのときはinlineにしちゃだめ。スクロールしないと見えない範囲が大きくなっちゃうように見えてPageSpeed Insights的にだめだよ！大きいファイルは遅延ロードしてね！(意訳)」ということらしい。大きいファイルってどう判断すりゃいいんだ？
- CSSの非同期ロードの方法は[この記事](https://qiita.com/rana_kualu/items/95a7adf8420ea2b9f657)でも書いてあるとおり結構簡単に実装できるっぽいのでそっちのほうがいいのかな？
- ~~なんか尻すぼみな備忘録になっちゃった...~~