[CSS in JS] Next.jsで利用しているEmotionで出力したCSSを最適化する

5 min read

背景

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 内の cssstring であるため、この文字列を何らかで最適化できると考えた。

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 - 構造の最適化を有効にする
  • forceMediaMerge:
    • Default: false
    • true - メディアクエリ(@media)をマージ
      「安全ではないけど、ほぼ問題なく動作するはず。自己責任」とのこと
  • comments:
    • Default: true
    • false - すべてのコメントを削除する

結果

Next.js + Emotion の環境で csso を利用して CSS を最適化できた。

csso のオプションによるものだが、適応したソースだと以下のような最適化が行われた。

  • 消しても問題ないクオートが消された
  • 省略可能な末尾のセミコロンが消された
  • メディアクエリがまとめられた
  • hsl()を利用していた箇所が HEX 値に変換された

PostCSS を利用してみた

PostCSS に加えて他プラグインを合わせて入れてみた。


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 を最適化できた。

最適化の結果はプラグインによって変わる。