[CSS in JS] Next.jsで利用しているEmotionで出力したCSSを最適化する
背景
Emotion は CSS の出力にstylisというプロセッサを利用している。ただ、デフォルトでは最適化を行わないため、出力される CSS は冗長になってしまう。stylis のプラグインを利用することで最適化は可能そうであるが、あまりプラグインが公開されておらず、思ったような最適化ができるようなものは見つからなかった。
方法
検証時の環境
- Next.js: v13.4.6
- Emotion: v11.11.0
前提
Next.js の SSR 時、Emotion が出力した CSS を事前に挿入するため pages/_document.tsx
内で以下のような定義をしている。
// pages/_document.tsx
// (中略)
const initialProps = await Document.getInitialProps(ctx);
const emotionStyles = extractCriticalToChunks(initialProps.html);
const emotionStyleTags = emotionStyles.styles.map(({ css, key, ids }) => {
return <style dangerouslySetInnerHTML={{ __html: css }} data-emotion={`${key} ${ids.join(' ')}`} key={key} />;
});
emotionStyles.styles.map
内の css
は string
であるため、この文字列を何らかで最適化できると考えた。
csso を利用してみた
cssoを利用してみた。
インストールは以下の通り。
npm install -D csso @types/csso
pages/_document.tsx
内に csso を以下のように組み込んだ。
// pages/_document.tsx
// (中略)
const initialProps = await Document.getInitialProps(ctx);
const emotionStyles = extractCriticalToChunks(initialProps.html);
const emotionStyleTags = emotionStyles.styles.map(({ css, key, ids }) => {
const ast = syntax.parse(css);
const compressedAst = syntax.compress(ast, {
restructure: true,
forceMediaMerge: true,
comments: false,
}).ast;
const minifiedCss = syntax.generate(compressedAst);
return <style dangerouslySetInnerHTML={{ __html: minifiedCss }} data-emotion={`${key} ${ids.join(' ')}`} key={key} />;
});
css
変数にはスタイル定義が文字列で入っているので、csso のsyntax.parse
にそのまま渡すだけでパースできる。parse.compress
で最適化を行い、parse.generate
で文字列に戻している。
const ast = syntax.parse(css);
const compressedAst = syntax.compress(ast, {
restructure: true,
forceMediaMerge: true,
comments: false,
}).ast;
const minifiedCss = syntax.generate(compressedAst);
今回指定したparse.compress
のオプションは以下の通り。
restructure
:- Default:
true
true
- 構造の最適化を有効にする
- Default:
forceMediaMerge
:- Default:
false
true
- メディアクエリ(@media
)をマージ
「安全ではないけど、ほぼ問題なく動作するはず。自己責任」とのこと
- Default:
comments
:- Default:
true
false
- すべてのコメントを削除する
- Default:
結果
Next.js + Emotion の環境で csso を利用して CSS を最適化できた。
csso のオプションによるものだが、適応したソースだと以下のような最適化が行われた。
- 消しても問題ないクオートが消された
- 省略可能な末尾のセミコロンが消された
- メディアクエリがまとめられた
hsl()
を利用していた箇所が HEX 値に変換された
PostCSS を利用してみた
PostCSS に加えて他プラグインを合わせて入れてみた。
- autoprefixer: 10.4.14
- cssnano: 6.0.1
- postcss: 8.4.24
- postcss-combine-duplicated-selectors: 10.0.3
- postcss-sort-media-queries: 5.2.0
pages/_document.tsx
内に PostCSS を以下のように組み込んだ。
// pages/_document.tsx
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';
import postcss from 'postcss';
import combineSelectors from 'postcss-combine-duplicated-selectors';
import postcssSortMediaQueries from 'postcss-sort-media-queries';
// (中略)
const initialProps = await Document.getInitialProps(ctx);
const emotionStyles = extractCriticalToChunks(initialProps.html);
const emotionStyleTags = emotionStyles.styles.map(({ css, key, ids }) => {
const processedCss = postcss([
autoprefixer({
overrideBrowserslist: packageJson.browserslist,
}),
cssnano({
preset: ['cssnano-preset-advanced'],
plugins: [],
}),
combineSelectors({ removeDuplicatedProperties: true }),
postcssSortMediaQueries,
]).process(css).css;
return (
<style dangerouslySetInnerHTML={{ __html: processedCss }} data-emotion={`${key} ${ids.join(' ')}`} key={key} />
);
});
postcss
関数の引数には PostCSS で利用するプラグインを配列で渡す。process
関数には最適化したい CSS(string)を渡す。
結果
Next.js + Emotion の環境で PostCSS を利用して CSS を最適化できた。
最適化の結果はプラグインによって変わる。