Runtime CSS in JSでスタイルを書くときは、動的なスタイリングを避けてパフォーマンスを落とさないようにしよう
はじめに
Runtime CSS in JS は、JavaScript の実行時にスタイルを適用する仕組みのためパフォーマンスに影響を及ぼすことがある。
想定環境
以下の CSS in JS ライブラリを想定。
他の Runtime CSS in JS ライブラリでも特に問題ないと思われる。
改善方法
propsを使った動的なスタイリングを使わない
props
を利用してスタイルを動的に変更することは一般的だが、都度計算処理が入るためパフォーマンスに影響がある。
// https://emotion.sh/docs/styled#changing-based-on-props
const Button = styled.button`
color: ${(props) => (props.primary ? "hotpink" : "turquoise")};
`;
render(
<Container>
<Button>This is a regular button.</Button>
<Button primary>This is a primary button.</Button>
</Container>
);
頻繁に状態が変化するコンポーネントでは、特に影響が大きくなる。
data属性を利用する
この方法ではdata
属性でスタイルを定義しており、ランタイムでのスタイル計算が不要となる。
const Button = styled.button`
color: turquoise;
&[data-primary="true"] {
color: hotpink;
}
`;
render(
<Container>
<Button>This is a regular button.</Button>
<Button data-primary={primary}>This is a primary button.</Button>
</Container>
);
props
での指定だと分岐が多く状態が分かりづらくなる。data
属性で状態をひとまとまりにする事で状態の可読性も上げることができる。
CSS Custom Properties(変数)を利用する
対象のコンポーネントに対して、インラインスタイルでCSS Custom Propertiesを定義して利用する方法もある。
const Button = styled.button`
color: var(--color);
`;
render(
<Container>
<Button
style={{
"--color": primary ? "hotpink" : "turquoise",
}}
>
This is a regular button.
</Button>
<Button
style={{
"--color": primary ? "hotpink" : "turquoise",
}}
>
This is a primary button.
</Button>
</Container>
);
ただし、同コンポーネント同士がネストされた場合に変数の値が意図しない上書きをされてしまう場合は、同じコンポーネントをネストしない等のような制限は必要かもしれない。
デフォルト値を指定しておく場合
var()
は、第一引数が定義されていない場合は第二引数に指定した代替値が適用される。
const Button = styled.button`
color: var(--color, turquoise);
`;
render(
<Container>
<Button>
This is a regular button.
</Button>
<Button
style={{
"--color": primary ? "hotpink" : "turquoise",
}}
>
This is a primary button.
</Button>
</Container>
);
デフォルト値をコンポーネント側に持たせない場合はvar()
に指定しても良いかもしれない。
attr()を利用する⚠
CSS Level 5 でattr()
で取得できる値が<string>
以外も扱えるようになった。執筆時点ではChrome 133でしか利用が出来ないが、今後利用範囲が広がるとprops
で渡さずともCSSのattr()
で完結できるケースが増えていくと思われる。
const Button = styled.button`
width: attr(data-size px);
`;
render(
<Button data-width="100px">This is a regular button.</Button>
);
Interpolated Selector(動的セレクタ)を使わない
以下のように、styled
コンポーネントのセレクタ内で他のコンポーネントを参照する記述(${Child}
)を「Interpolated Selector」などと呼ぶようである(各ライブラリのissueなどでそう表現されている)。
この方法だと先述と同様にランタイムでセレクタを解決するため、その分パフォーマンスが低下する。
// https://emotion.sh/docs/styled#targeting-another-emotion-component
const Child = styled.div`
color: red;
`;
const Parent = styled.div`
${Child} {
color: green;
}
`;
render(
<div>
<Parent>
<Child>Green because I am inside a Parent</Child>
</Parent>
<Child>Red because I am not inside a Parent</Child>
</div>
);
静的なCSSセレクタを利用する
styled
なコンポーネントに対してのスタイル定義ではなくなるが、CSSのネイティブな:has()
セレクタなどを活用し、JS の処理を介さずにスタイルを適用することでパフォーマンスを向上できる。
const Child = styled.div`
color: red;
`;
const Parent = styled.div`
& > * {
color: green;
}
`;
他にも同じようなケースで対象のコンポーネントの存在チェックなどを行うこともある。
const Parent = styled.div<{ hasChild }>`
${(props) => props.hasChild && "margin-top: 1rem"};
`;
render(<Parent hasChild={!!Text}>{<Child>{Text}</Child>}</Parent>);
こういった場合もJSで計算したりprops
を利用するのではなくCSSのネイティブなスタイルの利用も有用である。
const Parent = styled.div`
&:has(> *) {
margin-top: 1rem;
}
`;
グローバルスタイルをCSS in JS経由で定義しない
styled-componentsであればcreateGlobalStyle
、EmotionであればinjectGlobal
もしくはGlobal
を利用してグローバルスタイルを定義できる。
CSS in JSの機能にまとめておくと見通しが良いケースもあるが、いずれにしてもこれらの機能を介してスタイルを動的に変更することはできないため、CSS in JSを利用するメリットはほぼない。
- styled-components
- Emotion
グローバルなスタイル(リセットCSSなど)は、CSSファイルに定義してグローバルに読み込む方が良い。JSを介するオーバーヘッドの問題やファイルに対してキャッシュが効くことでの読み込み速度の改善など恩恵などが得られるケースがある。
まとめ
Runtime CSS in JS は、直感的にスタイルを記述することが出来るため柔軟な実装が可能である。しかし、それとはトレードオフでパフォーマンスへ影響を及ぼすような書き方が積み重なってしまうケースがある。
デザインやスタイルを注視して、動的な指定でなくとも良いケースは従来のCSSの定義を思い出して実装を行うと良い。
Discussion
実装によっては css 構文上の 変数部分を Custom Properties にしたり する実装もあるみたいで面白いですね。
Zero-Runtime CSS in JSはCustom Propertiesを駆使してRuntimeに近い動きにしてますね!