React でのスタイリングをどうするかはなかなか考え処であり、チャレンジングな分野である。
最近フロントエンドで話題になっている linaria というライブラリを使ってみたので、紹介も兼ねてここに記す。
また、後半では React におけるコンポーネント開発の手段として storybook を使用する上で、linaria の導入方法にも触れる。
linariaとは
- フロントエンドにスタイルを適用するための
CSS in JS
ライブラリ。 - styled-components のように、テンプレートリテラルの中に CSS をそのまま記述するという方式をとっている。
- CSS をほぼそのままコピペして突っ込むことが可能なため、デザイナーから渡される CSS を素早くプログラムに組み込める。
- ビルド時に静的に CSS ファイルを生成し、実行時にそれを読み込むため、一般的に styled-components よりも高速だと言われている。
styled-components との比較
主に同じ CSS in JS ライブラリである styled-components との比較をしてみた。
一応どちらも触ったことがありなるべく公平を期したつもりだが、現プロジェクトで linaria を使用しているため、若干バイアスがかかっているかもしれない。
項目 | linaria | styled-components |
---|---|---|
表示速度 | 速い(と言われている) | (linaria よりは)遅い(と言われている) |
CSS の生成タイミング | ビルド時 | 実行時 |
導入コスト | 少し高い1 | 低い |
GitHub スター数2 | 8.8k | 36.2k |
dynamic styling | △3 | ○ |
それぞれの開発者が GitHub 上で意見を交わしている記事はこちら。
linaria 導入
前提
以下の項目を導入済みであること
- Next.js 11.1.2
- TypeScript
インストール
linaria パッケージのインストールを行う。
npm i --save linaria@^3.0.0-beta.17 @linaria/core @linaria/react @linaria/babel-preset @linaria/shaker
linaria 本体のバージョンを指定せず最新バージョン(執筆時で2.3.1)をインストールすると上手くいかなかった。
.babelrc
に以下の記述をする。(まだない場合は作成する)
{
"presets": ["next/babel", "linaria/babel"]
}
linaria/babel は Next.js のデフォルト babel 設定には含まれないモジュールに依存しているため、それをインストールする必要がある。
npm install --save @babel/core
Next.js 用に設定する
以下のパッケージを導入する。
npm i --save next-linaria
next.config.js
にて以下の記述を行う。
const withLinaria = require('next-linaria');
module.exports = withLinaria({});
これにより、ビルド時に Next.js により linaria を使用して CSS ファイルを生成できるようになる。
この方のページ が参考になった。
記述例
シンプルなボタンコンポーネント
// components/Button.tsx にてコンポーネントを定義
import { styled } from '@linaria/react';
import React from 'react';
export const SimpleButton = ({ label, color, onClick }: { label: string; color?: string; onClick?: () => void }) => {
return (
<StyledButton color={color} onClick={onClick}>
{label}
</StyledButton>
);
};
const StyledButton = styled.span`
background-color: ${(p) => p.color ?? '#59add9'};
border-radius: 16px;
padding: 10px;
:hover {
background-color: ${(p) => p.color ?? '#3099cf'};
cursor: pointer;
}
`;
// --------------------------------------------------
// pages/index.tsx にて呼び出し
import {SimpleButton} from 'components/SimpleButton'
import React from 'react';
const Button = () => {
return (<SimpleButton label={"Simple Button"} />)
}
export default Button;
ビルド
npm run dev
or
npm run build
ビルドが成功すると、.linaria-cache
という隠しフォルダがプロジェクトルートに生成され、生成された CSS ファイルはその中に認められる。
以後、ソースコードが更新されるたびに、必要に応じて CSS ファイルが再生成される。(スタイルの記述に変更があった場合)
ちなみに、ビルドに失敗したり、成功して画面が表示されてもうまくスタイルが適用されていないなどの問題がある場合は、以下のコマンドでキャッシュをクリアした後再度ビルドするとうまく行ったりする。
rm -rf .next .linaria-cache
表示速度比較(linaria VS styled-components)
表示速度を計測するために、Google Chrome の拡張をインストールする。
https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en
比較1: シンプルなボタンを50個表示する場合
styled-components:
import React from 'react';
import styled from 'styled-components';
const Index = () => {
const n = 50;
const repeats = [...Array(n)];
return (
<div>
{repeats.map((x, i) => (
<SCButton key={i}>{`SCButton${i}`}</SCButton>
))}
</div>
);
};
export default Index;
const SCButton = styled.button`
margin: 10px 0px 0px 10px;
width: 100px;
height: 40px;
border-radius: 16px;
font-size: 12px;
`;
linaria:
import React from 'react';
import { styled as linariaStyled } from '@linaria/react';
const Index = () => {
const n = 50;
const repeats = [...Array(n)];
return (
<div>
{repeats.map((x, i) => (
<LinariaButton key={i}>{`LinariaButton${i}`}</LinariaButton>
))}
<br />
</div>
);
};
export default Index;
const LinariaButton = linariaStyled.button`
margin: 10px 0px 0px 10px;
width: 100px;
height: 40px;
border-radius: 16px;
font-size: 12px;
`;
実行例
コンポーネントの表示(linaria の場合。ただしどちらでもテキスト以外は全く同じ表示となる)
表示に 5.9ms かかっていることがわかる。
時間計測
これを npm run dev
、 npm run build
を実行した時について10回ずつ計測し、その平均を求めたのが以下(単位はms)。
- npm run dev
回数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | avg. |
---|---|---|---|---|---|---|---|---|---|---|---|
styled-components | 14.4 | 14.4 | 14.6 | 15.2 | 14.4 | 16 | 14.6 | 14.2 | 14.2 | 14.7 | 14.7 |
linaria | 12.2 | 11.7 | 11.7 | 12.1 | 10.3 | 11.5 | 11.4 | 11.6 | 10.7 | 12.1 | 11.5 |
14.7秒 → 11.5秒(22%の短縮)
- npm run build -- --profile && npm start
npm run dev | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | avg. |
---|---|---|---|---|---|---|---|---|---|---|---|
styled-components | 6.6 | 10.3 | 5.6 | 6.7 | 5.7 | 6.9 | 5.7 | 6.8 | 8.2 | 5.7 | 6.8 |
linaria | 4.7 | 6 | 6.8 | 5 | 5.8 | 6 | 5.1 | 5.8 | 7.9 | 5.1 | 5.8 |
6.8秒 → 5.8秒(15%の短縮)
比較2: プロパティを外部から注入するようなボタンを50個表示する場合
styled-components & linaria:
// -- コンポーネント部分は比較1と同じため省略 -- //
interface IProp {
color: string;
width: number;
}
// styled-components
const SCButton = styled.button<IProp>`
margin: 10px 0px 0px 10px;
width: ${(p) => p.width}px;
height: 40px;
color: ${(p) => p.color};
border-radius: 16px;
font-size: 12px;
`;
// linaria
const LinariaButton = linariaStyled.button<IProp>`
margin: 10px 0px 0px 10px;
width: ${(p) => p.width}px;
height: 40px;
color: ${(p) => p.color};
border-radius: 16px;
font-size: 12px;
`;
- npm run dev
回数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | avg. |
---|---|---|---|---|---|---|---|---|---|---|---|
styled-components | 13.9 | 16.3 | 13.6 | 15.8 | 16.2 | 13.5 | 15.7 | 15.8 | 15.1 | 15.9 | 15.9 |
linaria | 10.5 | 13.1 | 12.5 | 13 | 12.7 | 9.9 | 12.8 | 13 | 10.4 | 12.7 | 12.1 |
15.9秒 → 12.1秒(24%の短縮)
- npm run build -- --profile && npm start
回数 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | avg. |
---|---|---|---|---|---|---|---|---|---|---|---|
styled-components | 5.4 | 8.6 | 5.3 | 8.8 | 5.3 | 8.9 | 7.5 | 5.4 | 8.7 | 8.1 | 7.2 |
linaria | 4.7 | 6.6 | 4.2 | 6.5 | 7.5 | 4.8 | 6.4 | 7.7 | 4.3 | 7.1 | 6.0 |
7.2秒 → 6.0秒(17%の短縮)
考察
-
npm run build
よりもnpm run dev
時の方が linaria による表示速度改善の効果が高い。 - コンポーネントが複雑になる程、 linaria による表示速度改善の効果が高い。
stylelint によるテンプレートリテラルのフォーマット
上記のようにして記述した css(テンプレートリテラル) は、デフォルトではフォーマットが不十分である。そのためきちんとその辺りをやってくれるように設定を行う。
必要モジュールのインストール
npm install --save-dev @stylelint/postcss-css-in-js stylelint stylelint-config-prettier stylelint-config-recommended stylelint-order stylelint-prettier
.stylelint.json の設定
.stylelint.json
がない場合は作成
touch .stylelint.json
編集する
{
"extends": ["stylelint-config-recommended", "@linaria/stylelint", "stylelint-prettier/recommended"],
"overrides": [
{
"files": ["src/**/*.{jsx,tsx}"],
"customSyntax": "@stylelint/postcss-css-in-js"
}
],
"plugins": ["stylelint-order"],
"rules": {
"order/properties-alphabetical-order": true, // アルファベット順にソートする
"no-empty-source": null,
"no-descending-specificity": null, // 「詳細度が高いセレクタを後に書かなければならないルール」を無視。有効のままにしておくことを推奨
"block-no-empty": null
}
}
VSCode の拡張をインストールする
今回に限らず、複数人が参加するプロジェクトでは、拡張のインストールに併せてファイルに明示的に残しておくことをお勧めする。これにより、VSCode起動時にインストールされていない拡張が表示される。
{
"recommendations": [..., "stylelint.vscode-stylelint"]
}
VSCode 設定ファイル
{
"css.validate": false, // VSCode デフォルトのものと二重でlintがかかるのを避けるため
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.stylelint": true
},
"stylelint.validate": ["typescriptreact"]// tsxファイル内のテンプレートリテラルを対象とする
}
以上により、ファイル保存時にテンプレートリテラルのスタイルがフォーマットされるようになる。
linaria オプション
便利なカスタムオプションの紹介をする。
displayName
: 生成される css のクラス名に、元の関数コンポーネント名を付与する。(default: false)
以下のようにすることで、production ビルド時以外は displayName が付与される。開発時はブラウザ上でのディベロッパーツールを用いてソースのどのコンポーネントが実際にどのような形でレンダリングされているのかを頻繁に確認するため、ブラウザ上でのディベロッパーツールを用いたデバッグがやりやすくなる。
設定:
const withLinaria = require('next-linaria');
module.exports = withLinaria({
+ displayName: process.env.NODE_ENV !== 'production'// 本番環境以外で有効にする
});
コンポーネント例:
const Wrapper = styled.div`
...
`;
storybook で利用する
storybook とは
- コンポーネントの開発サイクルを速める目的で導入される。
- アプリ本体とは別でサーバーが起動され、UIも専用のものが立ち上がる。
- そのUI上では、コンポーネントが受け取るパラメータを自由に変更し、即動作確認を行える。4
storybook のインストール
npx sb init
これにより、.storybook
と src/stories
という二つのフォルダが生成される。
こちら のページが参考になる。
stories/XXX.stories.tsx でビルド済み CSS をインポート
stories/XXX.stories.tsx
は storybook に配置するコンポーネントの呼び出しロジックのようなもの。
つまり、linaria により生成された CSS を storybook で読み込むには、その設定が必要である。
なぜなら、storybook は Next.js とは別のコンテキストで動作するため、ビルド済み CSS の読み込み設定(webpack)が別途必要だからである。
具体的には .storybook/main.js
において以下のような記述を追加する。
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials'],
+ webpackFinal: (config) => {
+ ...
+ // enable linaria's css to be loaded as it is modified
+ config.module.rules.push({
+ test: /\.(tsx|ts|js|mjs|jsx)$/,
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: require.resolve('linaria/loader'),
+ options: {
+ sourceMap: process.env.NODE_ENV !== 'production',
+ ...(config.linaria || {}),
+ extension: '.linaria.module.css'
+ }
+ }
+ ]
+ });
+ ....
+ return config;
+ }
};
これは、Next.js での設定時に利用した next-linaria
パッケージの index.js
内の webpack 設定箇所をほぼそのまま流用している。
storybook の起動
npm run storybook
上述の設定により、起動中のソースコード(スタイル部分)の修正がそのまま反映UIにされる。
【Optional】 Next.js 12で SWC コンパイラを使用する
Next.js 最新のバージョン12では、Rust で記述された SWC
というコンパイラがネイティブで使用可能とのこと。(https://nextjs.org/blog/next-12#faster-builds-and-fast-refresh-with-rust-compiler)
これは従来の babel や terser といったツールをはるかに上回る速度を誇るらしく、ビルド時間短縮が期待されたため取り入れたみた。
その際、つまづいたポイントがあったため共有したい。
まず、Next.js 最新バージョンをインストールする。
npm install next@latest
プロジェクトに babel 設定ファイルが存在すると Next.js がビルドに SWC ではなく babel を使用するため、.babelrc
を削除する。(使用中の babel 設定ファイルが babel.config.js などの場合も同様)
rm -rf .babelrc
ちなみに、 babel によるビルドが行われている場合、以下のような出力が得られる。
info - Disabled SWC as replacement for Babel because of custom Babel configuration ".babelrc" https://nextjs.org/docs/messages/swc-disabled
Next.js 設定ファイルにて、SWC による minify 設定を行う。
const withLinaria = require('next-linaria');
module.exports = withLinaria({
+ swcMinify: true
});
この状態で一旦ビルドを行う。
npm run build
するとこのようなエラーが出た。
Syntax error: Unexpected token, expected ","
2 | import App, { AppProps, AppContext } from 'next/app';
3 |
> 4 | const MyApp = ({ Component, pageProps }: AppProps) => {
| ^
5 | return <Component {...pageProps} />;
6 | };
最初は戸惑ったが、どうやら TypeScript が上手くコンパイルできていない様子。SWC によるコンパイルは TypeScript の変換は含まれていないみたいである。
そのため、削除した .babelrc に存在した TypeScript 変換用の記述をどこかに追加しなければならない。
next.config.js で Webpack をカスタムしたり色々試したが、どれも上手くいかなかった。
最終的に、 babel の設定は package.json でも行えるという情報を手に入れ、試しに設定したところ、意図した挙動になった。
{
...
"babel": {
"presets": [
"next/babel"
]
}
}
再度ビルドを行うと、正常にビルドが成功し、出力からも SWC によるビルドが行われていることを確認した。
ビルド時間比較
SWC への変更前後のビルド時間を比較した。
各5回ずつ計測し、その平均値を出した。
- full-build(
rm -rf .next && npm run build
): 32秒 → 27.6秒(14%の短縮) - re-build(
npm run build
): 14.8秒 → 14.4秒(3%の短縮)
フルビルドの場合は一定のビルド時間の短縮は見込めそうだった。
所感
linaria と styled-components は、先に紹介した開発者の対談にあるように、どちらが優れているとかいう話ではないと思う。
また、React でのスタイルシステム構築には、CSS in JS 以外にも CSS モジュールやインライン方式、また従来のようにそのままクラス名を適用する、といったやり方がある。
実際、今回紹介した linaria と併用して、インラインスタイルなども結構書いていたりする。
都度その時に重要な指標は何かを見定め、ベターなチョイスを持って採用していくのが良いと感じている。