Why not login to Qiita and try out its useful features?

We'll deliver articles that match you.

You can read useful information later.

3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ReactAdvent Calendar 2022

Day 25

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

Last updated at Posted at 2022-12-24

概要

linaria という React のスタイリングライブラリについて以前記事を書いた。最近 Compiled という同様のライブラリがあることを知り、どのような違いがあるか気になったので少し触ってみた。

環境

  • Next.js(13.0.5)
  • React(18.2.0)
  • Storybook(6..22)

導入

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

$ npm install @compiled/react

webpack の設定

Next.js

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

$ 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;
  }
};

使用例

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

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 を利用することで解決できる。

interface I {
  color?: string;
}

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

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

@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 は使用できないため、別の方法で記述してくださいとのこと。

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

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

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

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

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

上記の Simple Button を画面上に 50個 表示させるページを用意した。
コンポーネントのレンダリング速度は、Google Chrome の拡張「React Developer Tools」で計測した。
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 コンパイラ) がやってくれているようだ。
と思ったら、swcMinify: false; をやっても minify された。どうやら、production ビルドではデフォルトで minify されるみたいである。

In Next.js, JavaScript and CSS files are automatically minified for production.

まとめ

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up

Qiita Advent Calendar is held!

Qiita Advent Calendar is an article posting event where you post articles by filling a calendar 🎅

Some calendars come with gifts and some gifts are drawn from all calendars 👀

Please tie the article to your calendar and let's enjoy Christmas together!

3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?