Emotion и SSR

MUI (Material-UI) использует Emotion в качестве системы стилизации по умолчанию. Это позволяет создавать динамические стили с использованием JavaScript, что обеспечивает гибкость и модульность компонентов. При серверном рендеринге (SSR) важно корректно собирать и внедрять стили, чтобы избежать мигания некорректно стилизованного контента (FOUC – Flash of Unstyled Content).


Установка необходимых пакетов

Для работы с MUI и Emotion в SSR необходимы следующие зависимости:

npm install @mui/material @emotion/react @emotion/styled @emotion/server
  • @mui/material – библиотека компонентов MUI.
  • @emotion/react – ядро Emotion для создания стилей.
  • @emotion/styled – возможность создавать стилизованные компоненты через шаблонные строки.
  • @emotion/server – сборка и инлайнинг стилей на сервере.

Создание Emotion Cache

Для корректного SSR требуется создать отдельный Emotion Cache, который будет использоваться как на клиенте, так и на сервере:

import createCache from '@emotion/cache';

export default function createEmotionCache() {
  return createCache({ key: 'css', prepend: true });
}
  • key: 'css' – префикс для всех классов, генерируемых Emotion.
  • prepend: true – вставляет стили в начало <head>, чтобы MUI переопределял стандартные стили браузера.

Настройка серверного рендеринга

Для SSR важно собрать все стили с помощью Emotion и вставить их в HTML до отправки на клиент. Пример для Next.js:

import createEmotionServer from '@emotion/server/create-instance';
import createEmotionCache from './createEmotionCache';
import Document, { Html, Head, Main, NextScript } from 'next/document';

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const cache = createEmotionCache();
    const { extractCriticalToChunks } = createEmotionServer(cache);

    const originalRenderPage = ctx.renderPage;

    ctx.renderPage = () =>
      originalRenderPage({
        enhanceApp: (App) => (props) => <App emotionCache={cache} {...props} />,
      });

    const initialProps = await Document.getInitialProps(ctx);
    const emotionStyles = extractCriticalToChunks(initialProps.html);
    const emotionStyleTags = emotionStyles.styles.map((style) => (
      <style
        data-emotion={`${style.key} ${style.ids.join(' ')}`}
        key={style.key}
        dangerouslySetInnerHTML={{ __html: style.css }}
      />
    ));

    return {
      ...initialProps,
      styles: [
        ...React.Children.toArray(initialProps.styles),
        ...emotionStyleTags,
      ],
    };
  }

  render() {
    return (
      <Html lang="ru">
        <Head />
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

Ключевые моменты:

  • createEmotionServer(cache) создает экземпляр для извлечения критических стилей.
  • extractCriticalToChunks анализирует HTML и возвращает минимальный набор стилей, необходимых для корректного отображения.
  • data-emotion позволяет Emotion на клиенте идентифицировать уже встроенные стили и избежать дублирования.

Использование Emotion Cache на клиенте

На клиенте необходимо передать созданный кеш в ThemeProvider:

import { CacheProvider } from '@emotion/react';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import createEmotionCache from './createEmotionCache';

const clientSideEmotionCache = createEmotionCache();
const theme = createTheme();

export default function MyApp({ Component, pageProps, emotionCache = clientSideEmotionCache }) {
  return (
    <CacheProvider value={emotionCache}>
      <ThemeProvider theme={theme}>
        <Component {...pageProps} />
      </ThemeProvider>
    </CacheProvider>
  );
}
  • Использование единого CacheProvider обеспечивает идентичность стилей на сервере и клиенте.
  • createTheme() позволяет кастомизировать тему, интегрированную с MUI.

Работа с динамическими стилями

Emotion поддерживает динамические стили через sx и styled API:

import { styled } from '@mui/material/styles';
import Button from '@mui/material/Button';

const CustomButton = styled(Button)(({ theme, color }) => ({
  backgroundColor: color || theme.palette.primary.main,
  '&:hover': {
    backgroundColor: theme.palette.secondary.main,
  },
}));
  • Параметры темы (theme) доступны внутри функции.
  • Возможность использовать пропсы (color) для динамического изменения стилей.

SSR корректно собирает эти стили благодаря переданному emotionCache и extractCriticalToChunks.


Инлайнинг стилей и оптимизация

Инлайнинг стилей на сервере снижает время первого отображения (TTFB) и предотвращает FOUC. Основные рекомендации:

  1. Использовать один общий Emotion Cache для всего приложения.
  2. Извлекать критические стили через extractCriticalToChunks.
  3. Передавать стили через <style data-emotion> в <Head>.
  4. Минимизировать использование динамических стилей на первом рендере без необходимости.

Интеграция с TypeScript

TypeScript поддерживает Emotion и MUI без сложностей:

import createCache, { EmotionCache } from '@emotion/cache';

export default function createEmotionCache(): EmotionCache {
  return createCache({ key: 'css', prepend: true });
}
  • Типизация EmotionCache обеспечивает автодополнение и контроль пропсов.
  • Типы MUI корректно сочетаются с ThemeProvider и styled.

Особенности при использовании SSR

  • Флешинг стилей: без extractCriticalToChunks пользователь может увидеть неподготовленные стили.
  • Гидратация: клиент должен повторно использовать серверный кеш, иначе Emotion создаст новые классы, что приведет к несовпадению HTML и стилей.
  • Производительность: хранение кеша позволяет снизить количество пересчетов CSS и ускорить рендер.

Практическое применение

  • Создание корпоративных приложений с MUI, где критически важно корректное отображение на первом рендере.
  • Интеграция с Next.js, Remix или другими фреймворками SSR.
  • Использование styled и sx для построения адаптивных компонентов, полностью совместимых с темами и серверным рендерингом.

Корректная организация Emotion и MUI в SSR обеспечивает стабильную, производительную и предсказуемую визуализацию интерфейсов без лишних миганий стилей.