概要
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 の設定を記述する。
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 でも使用したい場合は、以下のような記述により有効化できる。
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;
`;
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
となる。
- Next.js のプロジェクトをビルドすると、ビルド完了時に生成ファイルの概要のようなものがコンソール上に表示される。その中でも、
-
コンポーネントを呼び出しているページ
- 今回は
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 の設定ファイルに以下のように追記する。
+ 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 の設定ファイルを以下のようにする。
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 を超える有用性は感じなかった。
もっと使い道があると思うので、機会があれば触ってみたいと思う。