Compiled という CSS in JS ライブラリを Next.js プロジェクトで使ってみた

# 概要
`linaria` という React のスタイリングライブラリについて以前[記事](https://qiita.com/yaskitie/items/b1aec0d4f9c1fd598634)を書いた。最近 `Compiled` という同様のライブラリがあることを知り、どのような違いがあるか気になったので少し触ってみた。

https://compiledcssinjs.com/

# 環境
- Next.js(13.0.5)
- React(18.2.0)
- Storybook(6..22)

# 導入

## パッケージのインストール

```sh
$ npm install @compiled/react
```

## webpack の設定

### Next.js 

まず webpack 関係のパッケージをインストールする。

```sh
$ npm install -D @compiled/webpack-loader babel-loader
```

Next.js の設定ファイルに webpack の設定を記述する。

```next.config.js
module.exports = {
  webpack: (config, options) => {
    ...
    config.module.rules.push({
      test: /\.(js|ts|tsx)$/,
      exclude: /node_modules/,
      use: [
        { loader: 'babel-loader' },
        {
          loader: '@compiled/webpack-loader'
        }
      ]
    });
    ...
    return config;
  }
};
```

これで Next.js のアプリで Compiled を使用できるようになった。

### storybook(Optional)

storybook でも使用したい場合は、以下のような記述により有効化できる。

```main.js
const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

module.exports = {
  core: {
    builder: 'webpack5'
  },
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials'
  ],
  staticDirs: ['../public'],
  webpackFinal: (config) => {
    config.resolve.plugins = [
      ...(config.resolve.plugins || []),
      new TsconfigPathsPlugin({
        extensions: config.resolve.extensions
      })
    ];
    // -------- 追加 --------
    config.module.rules.push({
      test: /\.(tsx|ts|js|mjs|jsx)$/,
      exclude: /node_modules/,
      use: [
        {
          loader: require.resolve('@compiled/webpack-loader')
        }
      ]
    });
    // -------- 追加 --------
    return config;
  }
};
```

# 使用例

## シンプルなボタンコンポーネント

```tsx
import { styled } from '@compiled/react';
import React from 'react';

// コンポーネントの呼び出し
const Index = () => {
  return <SimpleButton label={'Simple Button'} />;
};
export default Index;

// コンポーネントの定義
const SimpleButton = ({ label, color, onClick }: { label: string; color?: string; onClick?: () => void }) => {
  return (
    <StyledButton color={color} onClick={onClick}>
      {label}
    </StyledButton>
  );
};

// Compiled によるスタイルコンポーネントの生成
const StyledButton = styled.span`
  background-color: ${(p) => p.color ?? '#59add9'};/* warning */
  border-radius: 16px;
  padding: 10px;
`;
```

まずこの段階で、`p.color` の部分に関して、以下の警告が出るはずだ。

> **Property 'color' does not exist on type '{}'.ts(2339)**

これは Compiled が React の JSX タグに対して使用できるプロパティを類推してくれないことに起因するようだ(今回の場合、`span` が color を類推してくれない)。
この問題は、interface や type を利用することで解決できる。

```tsx
interface I {
  color?: string;
}

const StyledButton = styled.span<I>`
  background-color: ${(p) => p.color ?? '#59add9'};
  border-radius: 16px;
  padding: 10px;
`;
```

先ほどの警告はなくなった。しかし、今度はコンパイルエラーとなる。
画面には以下のようなものが表示される。

:::note alert
@compiled/compiled-loader - Unhandled exception
  SyntaxError: .../src/pages/index.tsx: A logical expression contains an invalid CSS declaration.
      Compiled doesn't support CSS properties that are defined with a conditional rule that doesn't specify a default value.
      Eg. font-weight: ${(props) => (props.isPrimary && props.isMaybe) && 'bold'}; is invalid.
      Use ${(props) => props.isPrimary && props.isMaybe && ({ 'font-weight': 'bold' })}; instead (22:21).
:::

内容を見てみると、Compiled では デフォルト値を指定しない **conditional rule** は使用できないため、別の方法で記述してくださいとのこと。

言われたように、変更してみる。

```tsx
const StyledButton = styled.span<I>`
  ${(p) => p.color && { backgroundColor: p.color }};   // (1)color が定義されているときはそれを使い、
  ${(p) => !p.color && { backgroundColor: '#59add9' }};// (2)color が定義されていないときはデフォルト値を使う
  border-radius: 16px;
  padding: 10px;
`;
```

これでボタンの表示は上手くいくようになった。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/671630/cdd6b242-f898-567a-a153-283c6b0f0ffa.png)

`background-color` のように、コンポーネントに渡す値を指定しなかった場合のデフォルト値も(システムのデフォルト値ではなく自分で)指定したい場合、２行に分けて書く必要がありそうだ。
上の例では、(2)の行がない場合、color が渡されないと `background-color: #ffffff` となる。
このようなユースケースに対してスマートに記述できない点は気になるポイントである。

# linaria とのパフォーマンス比較

## コンポーネントのレンダリング時間

上記の **Simple Button を画面上に 50個 表示させるページ**を用意した。
コンポーネントのレンダリング速度は、Google Chrome の拡張「[React Developer Tools](https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en)」で計測した。
10回計測した平均値を示す。

### development build

| ライブラリ | レンダリング時間平均値 |
|:-:|:-:|
| linaria |  10.9s |
| Compiled  | 13.9s  |

### production build

| ライブラリ | レンダリング時間平均値 |
|:-:|:-:|
| linaria |  5.4s |
| Compiled  | 7.0s  |

以上より、**Compiled の方が linaria よりレンダリングには時間がかかる**ということがわかった。

## ビルド時間

`next build` によるビルド時間を計測した。
初回のビルド時間(1回のみ計測)と、(キャッシュの効いている)2回目以降(5回計測し、その平均値)を示す。

| ライブラリ | ビルド時間(初回) | ビルド時間(2回目以降) |
|:-:|:-:|:-:|
| linaria |  22s | 9.0s |
| Compiled  | 27s  | 8.6s |

以上より、**初回ビルドは Compiled の方が linaria よりも遅く、2回目以降のビルドでは両者ほぼ同じ**ということがわかった。

## 生成ファイルサイズ

以下の項目のサイズを調べた。

- First Load JS shared by all
    - Next.js のプロジェクトをビルドすると、ビルド完了時に生成ファイルの概要のようなものがコンソール上に表示される。その中でも、`_app.tsx` に関わる、全てのページで必要な共有ファイルとしてまとめられるのが `First Load JS shared by all` となる。

- コンポーネントを呼び出しているページ
    - 今回は `index.js`(`.next/static/chunks/pages/index.js`)。
    - development/production ビルドの両方について確認した。


| ライブラリ | First Load JS shared by all | index.js(development) | index.js(production) |
|:-:|:-:|:-:|:-:|
| linaria | 127KB  | 64KB |  6KB |
| Compiled  | 130KB  | 102KB | 5KB |

以上より、development ビルド時のコンポーネント呼び出しファイルのサイズに大きな差があり、Compiled の方がサイズが大きくなることがわかった。(production ビルドでも2割程度の差異は見られるが、サイズ自体が小さいため影響は少ないと考えられる)

# Compiled のビルド設定など紹介

これまでレンダリング速度やビルド時間、生成ファイルなどの観点から linaria と比較してきた結果をみると、Compiled にメリットがないのではないか、という印象を持った。以降では汚名返上ではないが、生成される CSS の設定オプションについて見て行きたい。

## CSS Extraction

Compiled では、生成された　style は、該当ページの `<head>` 下の `<style>` タグとして css が挿入される。
つまり、同じコンポーネントを参照している場合でも、それらが複数のページから呼び出されればその分だけそのページの `<head>` に挿入される形となる。
CSS Extraction という機能を利用することで、生成された css を共通の場所にまとめることができる。
Next.js の設定ファイルに以下のように追記する。

```next.config.js
+ const { CompiledExtractPlugin } = require('@compiled/webpack-loader');

module.exports = {
  webpack: (config, options) => {
    ...
    config.module.rules.push({
      test: /\.(js|ts|tsx)$/,
      exclude: /node_modules/,
      use: [
        { loader: 'babel-loader' },
        {
          loader: '@compiled/webpack-loader',
+         options: {
+           extract: true
+         }
        }
      ]
    });
+   config.plugins.push(new CompiledExtractPlugin());
    ...
    return config;
  }
};
```

これにより、development ビルド時には `webpack://node_modules/@compiled/webpack-loader/css-loader/compiled-css.css` に各ページの css が集約され、production ビルド時には `{host}/_next/static/css/[hash].css` に集約される。


## CSS Minimization

生成された css を minify してくれる機能。
Next.js の設定ファイルを以下のようにする。

```next.config.js
const { CompiledExtractPlugin } = require('@compiled/webpack-loader');
+ const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
  webpack: (config, options) => {
    ...
    config.module.rules.push({
      test: /\.(js|ts|tsx)$/,
      exclude: /node_modules/,
      use: [
        { loader: 'babel-loader' },
        {
          loader: '@compiled/webpack-loader',
          options: {
            extract: true
          }
        }
      ]
    });
    config.plugins.push(new CompiledExtractPlugin());
+   config.optimization.minimizer.push(new CssMinimizerPlugin())
    ...
    return config;
  }
};
```

これで、`_next/static/css/[hash].css` に minify された形で格納される。
ただし、この設定がなくても minify されて出力されたので、[Next.js(SWC コンパイラ) がやってくれているようだ。](https://nextjs.org/docs/advanced-features/compiler#minification)
と思ったら、`swcMinify: false;` をやっても minify された。どうやら、production ビルドではデフォルトで minify されるみたいである。

https://nextjs.org/learn/foundations/how-nextjs-works/minifying
> In Next.js, JavaScript and CSS files are automatically minified for production.

# まとめ
調べた限りでは、CSS in JS ライブラリとしての linaria を超える有用性は感じなかった。
もっと使い道があると思うので、機会があれば触ってみたいと思う。