Emotion × Next.js:サーバーサイドレンダリング対応のベストプラクティス

モダンな Web アプリケーション開発において、パフォーマンスとユーザー体験の向上は避けて通れない課題です。特に、CSS-in-JS ライブラリである Emotion と Next.js を組み合わせる際、サーバーサイドレンダリング(SSR)対応は多くの開発者が直面する壁となっています。
この記事では、Emotion と Next.js を組み合わせた際の SSR 対応について、実際のエラーとその解決策を含めて詳しく解説します。初心者の方でも理解しやすいよう、段階的に説明していきますので、ぜひ最後までお付き合いください。
Emotion と Next.js の基本理解
Emotion とは
Emotion は、JavaScript で CSS を記述できる CSS-in-JS ライブラリです。React との相性が良く、動的なスタイリングやテーマ機能を簡単に実装できることが特徴です。
javascript// Emotionの基本的な使用例
import styled from '@emotion/styled';
const Button = styled.button`
background-color: #007bff;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
&:hover {
background-color: #0056b3;
}
`;
Emotion の魅力は、JavaScript の変数や関数を直接 CSS に組み込めることです。これにより、動的なスタイリングが非常に簡単になります。
Next.js の SSR とは
Next.js のサーバーサイドレンダリング(SSR)は、ページの HTML をサーバー側で事前に生成し、クライアントに送信する技術です。これにより、初期表示速度の向上と SEO 対策が実現できます。
javascript// Next.jsのSSRページ例
export default function HomePage({ data }) {
return (
<div>
<h1>サーバーサイドで生成されたコンテンツ</h1>
<p>{data.message}</p>
</div>
);
}
// サーバーサイドでデータを取得
export async function getServerSideProps() {
const data = await fetchData();
return {
props: { data },
};
}
SSR の最大のメリットは、検索エンジンがページの内容を正確に理解できることです。これにより、SEO スコアの向上が期待できます。
組み合わせるメリット
Emotion と Next.js を組み合わせることで、以下のようなメリットが得られます:
- 動的スタイリング: JavaScript の変数や状態に基づいたスタイル変更
- コンポーネント化: スタイルとロジックを一つのファイルにまとめられる
- テーマ機能: アプリケーション全体で一貫したデザインシステムの構築
- 開発効率: CSS ファイルの管理が不要になり、開発速度が向上
しかし、この組み合わせには SSR 対応という課題が待ち受けています。
SSR 対応における Emotion の課題
サーバーサイドでのスタイル生成
Emotion を Next.js で使用する際、最も大きな課題はサーバーサイドでのスタイル生成です。通常の CSS-in-JS はクライアントサイドで動作するため、サーバーサイドではスタイルが適用されません。
javascript// 問題のあるコード例
import styled from '@emotion/styled';
const StyledComponent = styled.div`
background-color: blue;
color: white;
`;
export default function Page() {
return <StyledComponent>Hello World</StyledComponent>;
}
このコードを SSR で実行すると、以下のような問題が発生します:
- サーバーサイドではスタイルが適用されない
- クライアントサイドでハイドレーション時にスタイルが適用される
- 一瞬スタイルが適用されていない状態が見える(FOUC: Flash of Unstyled Content)
ハイドレーション問題
ハイドレーションとは、サーバーサイドで生成された HTML にクライアントサイドの JavaScript を適用するプロセスです。Emotion の場合、この過程でスタイルの不一致が発生することがあります。
javascript// ハイドレーションエラーの例
// サーバーサイドとクライアントサイドで異なるスタイルが生成される
const DynamicComponent = styled.div`
background-color: ${(props) => props.theme.primary};
color: ${(props) => props.theme.text};
`;
このような動的スタイルを使用すると、サーバーサイドとクライアントサイドで異なるスタイルが生成され、ハイドレーションエラーが発生する可能性があります。
パフォーマンスへの影響
SSR 対応を適切に行わない場合、以下のようなパフォーマンス問題が発生します:
- バンドルサイズの増大: 不要なスタイルコードが含まれる
- レンダリング速度の低下: スタイルの再計算が頻繁に発生
- メモリ使用量の増加: スタイルキャッシュが適切に管理されない
解決策:@emotion/react と@emotion/server
必要なパッケージの導入
Emotion の SSR 対応には、以下のパッケージが必要です:
bashyarn add @emotion/react @emotion/styled @emotion/server
これらのパッケージの役割は以下の通りです:
@emotion/react
: React 用の Emotion ライブラリ@emotion/styled
: styled-components 風の API@emotion/server
: サーバーサイドでのスタイル抽出
サーバーサイド設定
Next.js の_document.tsx
ファイルでサーバーサイドの設定を行います:
javascript// pages/_document.tsx
import Document, {
Html,
Head,
Main,
NextScript,
} from 'next/document';
import { extractCritical } from '@emotion/server';
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(
ctx
);
const critical = extractCritical(initialProps.html);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style
data-emotion-css={critical.ids.join(' ')}
dangerouslySetInnerHTML={{
__html: critical.css,
}}
/>
</>
),
};
}
render() {
return (
<Html>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
この設定により、サーバーサイドで生成されたスタイルが HTML に埋め込まれ、クライアントサイドでのハイドレーションがスムーズに行われます。
クライアントサイド設定
_app.tsx
ファイルでクライアントサイドの設定を行います:
javascript// pages/_app.tsx
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
const clientSideEmotionCache = createCache({ key: 'css' });
export default function MyApp({
Component,
pageProps,
emotionCache = clientSideEmotionCache,
}) {
return (
<CacheProvider value={emotionCache}>
<Component {...pageProps} />
</CacheProvider>
);
}
この設定により、クライアントサイドでのスタイル管理が適切に行われ、ハイドレーションエラーを防ぐことができます。
実装例:基本的な SSR 対応
_document.tsx の設定
より詳細な_document.tsx
の設定例をご紹介します:
javascript// pages/_document.tsx
import Document, {
Html,
Head,
Main,
NextScript,
} from 'next/document';
import { extractCriticalToChunks } from '@emotion/server';
import createEmotionServer from '@emotion/server/create-instance';
import createCache from '@emotion/cache';
const cache = createCache({ key: 'css' });
const { extractCritical } = createEmotionServer(cache);
export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const originalRenderPage = ctx.renderPage;
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
<App emotionCache={cache} {...props} />,
});
const initialProps = await Document.getInitialProps(
ctx
);
const emotionStyles = extractCritical(
initialProps.html
);
return {
...initialProps,
styles: (
<>
{initialProps.styles}
<style
data-emotion={`css ${emotionStyles.ids.join(
' '
)}`}
dangerouslySetInnerHTML={{
__html: emotionStyles.css,
}}
/>
</>
),
};
}
render() {
return (
<Html lang='ja'>
<Head>
<meta charSet='utf-8' />
<link rel='icon' href='/favicon.ico' />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
}
この設定では、より詳細なキャッシュ管理とスタイル抽出を行っています。
_app.tsx の設定
_app.tsx
でのより高度な設定例です:
javascript// pages/_app.tsx
import { CacheProvider } from '@emotion/react';
import { ThemeProvider } from '@emotion/react';
import createCache from '@emotion/cache';
// クライアントサイドのキャッシュを作成
const clientSideEmotionCache = createCache({
key: 'css',
prepend: true, // CSSの優先順位を上げる
});
// テーマの定義
const theme = {
colors: {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
},
spacing: {
small: '8px',
medium: '16px',
large: '24px',
},
};
export default function MyApp({
Component,
pageProps,
emotionCache = clientSideEmotionCache,
}) {
return (
<CacheProvider value={emotionCache}>
<ThemeProvider theme={theme}>
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
);
}
この設定により、テーマ機能も活用できるようになります。
コンポーネントでの使用例
実際のコンポーネントでの使用例をご紹介します:
javascript// components/Button.jsx
import styled from '@emotion/styled';
const StyledButton = styled.button`
background-color: ${(props) =>
props.variant === 'primary'
? props.theme.colors.primary
: props.theme.colors.secondary};
color: white;
padding: ${(props) => props.theme.spacing.medium};
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s ease;
&:hover {
background-color: ${(props) =>
props.variant === 'primary' ? '#0056b3' : '#545b62'};
}
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
`;
export default function Button({
children,
variant = 'primary',
...props
}) {
return (
<StyledButton variant={variant} {...props}>
{children}
</StyledButton>
);
}
このコンポーネントは、テーマを使用して動的なスタイリングを行い、SSR でも適切に動作します。
高度な設定と最適化
キャッシュ戦略
パフォーマンスを向上させるためのキャッシュ戦略を実装します:
javascript// utils/emotionCache.js
import createCache from '@emotion/cache';
// 本番環境でのキャッシュ設定
const isBrowser = typeof document !== 'undefined';
export default function createEmotionCache() {
let insertionPoint;
if (isBrowser) {
const emotionInsertionPoint = document.querySelector(
'meta[name="emotion-insertion-point"]'
);
insertionPoint = emotionInsertionPoint ?? undefined;
}
return createCache({
key: 'mui-style',
insertionPoint,
prepend: true,
});
}
このキャッシュ設定により、スタイルの挿入順序を制御し、CSS の優先順位を適切に管理できます。
動的スタイルの処理
動的スタイルを安全に処理する方法をご紹介します:
javascript// components/DynamicCard.jsx
import styled from '@emotion/styled';
const Card = styled.div`
background-color: ${(props) => {
// サーバーサイドとクライアントサイドで一貫性を保つ
if (typeof window === 'undefined') {
return '#ffffff'; // サーバーサイドでのデフォルト値
}
return props.isDark ? '#2c3e50' : '#ffffff';
}};
color: ${(props) =>
props.isDark ? '#ffffff' : '#2c3e50'};
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
`;
export default function DynamicCard({ isDark, children }) {
return <Card isDark={isDark}>{children}</Card>;
}
この実装では、サーバーサイドとクライアントサイドで一貫したスタイルを保つため、条件分岐を使用しています。
パフォーマンス最適化
バンドルサイズとレンダリング速度を最適化する設定です:
javascript// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')(
{
enabled: process.env.ANALYZE === 'true',
}
);
module.exports = withBundleAnalyzer({
// Emotionの最適化設定
experimental: {
optimizeCss: true,
},
// 不要なCSSの削除
webpack: (config, { dev, isServer }) => {
if (!dev && !isServer) {
config.optimization.splitChunks.cacheGroups.styles = {
name: 'styles',
test: /\.(css|scss)$/,
chunks: 'all',
enforce: true,
};
}
return config;
},
});
この設定により、本番環境での CSS バンドルが最適化され、パフォーマンスが向上します。
よくある問題とトラブルシューティング
スタイルの不一致
最もよく発生する問題は、サーバーサイドとクライアントサイドでのスタイルの不一致です。
エラー例:
arduinoWarning: Text content did not match. Server: "Hello" Client: "Hello"
解決策:
javascript// コンポーネントでの解決例
import { useEffect, useState } from 'react';
import styled from '@emotion/styled';
const DynamicComponent = styled.div`
background-color: ${(props) => props.bgColor};
`;
export default function MyComponent() {
const [mounted, setMounted] = useState(false);
const [bgColor, setBgColor] = useState('#ffffff'); // デフォルト値
useEffect(() => {
setMounted(true);
// クライアントサイドでのみ実行
const userPreference = localStorage.getItem('theme');
if (userPreference === 'dark') {
setBgColor('#2c3e50');
}
}, []);
// サーバーサイドではデフォルト値を表示
if (!mounted) {
return (
<DynamicComponent bgColor='#ffffff'>
Loading...
</DynamicComponent>
);
}
return (
<DynamicComponent bgColor={bgColor}>
Content
</DynamicComponent>
);
}
メモリリーク対策
長時間動作するアプリケーションでのメモリリークを防ぐ設定です:
javascript// utils/emotionCache.js
import createCache from '@emotion/cache';
let emotionCache;
if (typeof window !== 'undefined') {
// クライアントサイドでのみキャッシュを作成
emotionCache = createCache({
key: 'css',
prepend: true,
// メモリリークを防ぐための設定
stylisPlugins: [],
});
}
export default emotionCache;
デバッグ手法
Emotion のデバッグを効率的に行うための設定です:
javascript// pages/_app.tsx
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
const clientSideEmotionCache = createCache({
key: 'css',
// デバッグモードの有効化
...(process.env.NODE_ENV === 'development' && {
stylisPlugins: [
// 開発環境でのスタイルデバッグ
(context, content, selectors) => {
if (context === 1) {
console.log('Generated CSS:', content);
}
},
],
}),
});
export default function MyApp({
Component,
pageProps,
emotionCache = clientSideEmotionCache,
}) {
return (
<CacheProvider value={emotionCache}>
<Component {...pageProps} />
</CacheProvider>
);
}
まとめ
Emotion と Next.js の SSR 対応は、最初は複雑に感じるかもしれませんが、適切な設定とベストプラクティスを理解することで、パフォーマンスの高いアプリケーションを構築できます。
今回ご紹介した内容を実践することで、以下のような成果が期待できます:
- SEO 対策: サーバーサイドでのスタイル適用により、検索エンジンが正確にページを認識
- パフォーマンス向上: 適切なキャッシュ戦略により、レンダリング速度が向上
- 開発効率: コンポーネントベースのスタイリングにより、保守性が向上
- ユーザー体験: FOUC の解消により、スムーズなページ表示を実現
実際のプロジェクトでこれらの設定を適用する際は、段階的に導入することをお勧めします。まずは基本的な SSR 対応から始め、徐々に高度な最適化を適用していくことで、安全に移行できます。
Emotion と Next.js の組み合わせは、モダンな Web アプリケーション開発において非常に強力なツールです。この記事で学んだ知識を活用して、素晴らしいアプリケーションを作成してください。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来