Skip to content

Conversation

siriwatknp
Copy link
Member

@siriwatknp siriwatknp commented Oct 13, 2025

closes #42772

For Reviewer

  • Moved Theme to stylesOptimized to remove cyclic deps and set theme.components to empty
  • Rexport the Theme from styles with augmented theme.components to preserve the behavior
  • Mirror the export of stylesOptimized to styles so that user can switch the import path without breaking change
  • Update all <component>.d.ts to use from stylesOptimized and export its Theme types for selective augmentation

Summary

  • No changes for existing user
  • For user who wants to optimize TS instantiation time, do the following:
    • Replace every @mui/material/styles import with @mui/material/stylesOptimized including module augmentation
    • Selectively augment component to the theme for autocompletion
      import { ButtonTheme } from '@mui/material/Button';
      import { createTheme } from '@mui/material/stylesOptimized";
      
      declare module "@mui/material/stylesOptimized" {
        interface ThemeComponents extends ButtonTheme {}
      }
      
      createTheme({
        components: {
          //. ✅ type-safe
          MuiButton: {}
        }
      })

To test the change, checkout this PR:

cd packages/mui-material/perf-test
npx tsc --noEmit --diagnostics

Then edit the packages/mui-material/perf-test/test-createTheme.tsx to import createTheme from @mui/material/styles and run diagnosis again.

Compare the result between the two.

Root Cause

The issue stems from circular TypeScript dependency in the type definitions:

Original definition (packages/mui-material/src/styles/createThemeNoVars.d.ts):

export interface ThemeOptions extends Omit<SystemThemeOptions, 'zIndex'>, CssVarsOptions {
  components?: Components<Omit<Theme, 'components'>>; // ← References Theme
  // ... other properties
}

export interface BaseTheme extends SystemTheme {
  mixins: Mixins;
  palette: Palette & (CssThemeVariables extends { enabled: true } ? CssVarsPalette : {});
  shadows: Shadows;
  transitions: Transitions;
  typography: TypographyVariants;
  zIndex: ZIndex;
  unstable_strictMode?: boolean;
}

export interface Theme extends BaseTheme, CssVarsProperties {
  components?: Components<BaseTheme>; // ← Used by ThemeOptions
  // ... other properties
}

The circular path:

  1. ThemeOptions.componentsComponents<Omit<Theme, 'components'>>
  2. This requires resolving the full Theme interface
  3. Theme extends BaseTheme and CssVarsProperties, inlining all their type definitions
  4. Theme is referenced back in ThemeOptionscircular dependency

Why exponential type computation:

The Components<Theme> interface is massive - for each of 80+ MUI components, it references:

export interface Components<Theme = unknown> {
  MuiButton?: {
    defaultProps?: ComponentsProps['MuiButton']; // ← Button's props interface
    styleOverrides?: ComponentsOverrides<Theme>['MuiButton']; // ← Needs Theme generic
    variants?: ComponentsVariants<Theme>['MuiButton']; // ← Needs Theme generic
  };
  MuiCard?: {
    defaultProps?: ComponentsProps['MuiCard']; // ← Card's props interface
    styleOverrides?: ComponentsOverrides<Theme>['MuiCard']; // ← Needs Theme generic
    variants?: ComponentsVariants<Theme>['MuiCard']; // ← Needs Theme generic
  };
  // ... 80+ more components
}

When TypeScript resolves Components<Omit<Theme, 'components'>>:

  1. It must instantiate all 80+ component definitions
  2. Each component references its full Props interface (from the actual component file)
  3. Each ComponentsOverrides and ComponentsVariants uses the Theme generic with complex Interpolation types
  4. The circular dependency causes TypeScript to repeatedly re-instantiate this massive type
  5. During Webpack builds with ts-loader, these types are resolved for every module importing from @mui/material

Memory spike: From ~460MB to ~2.2GB (4× increase), causing OOM errors in CI/CD

User Journey:

// ❌ ANY import from @mui/material/styles triggers memory spike
import { createTheme, ThemeOptions } from '@mui/material/styles';
//       ^^^^^^^^^^^ ← Even just importing createTheme loads circular types
export const themeOptions: ThemeOptions = {
  palette: { primary: { main: '#1976d2' } },
};
// Webpack with ts-loader: 2.2GB heap usage

// ✅ Real solution: Use stylesOptimized entry point
import { createTheme, ThemeOptions } from '@mui/material/stylesOptimized';
export const themeOptions: ThemeOptions = {
  palette: { primary: { main: '#1976d2' } },
};
// Webpack with ts-loader: ~460MB heap usage (normal)

Solution

Created alternative entry point stylesOptimized that breaks circular dependency by moving complete Theme definition there, making createThemeNoVars.d.ts reference it instead of defining inline.

Key Changes

1. New optimized entry point (packages/mui-material/src/stylesOptimized/createTheme.d.ts):

// Define complete Theme and ThemeOptions without circular dependency
export interface ThemeComponents {
  mergeClassNameAndStyles?: boolean;
  [componentName: string]: any;
}

export interface ThemeOptions extends Omit<SystemThemeOptions, 'zIndex'>, CssVarsOptions {
  components?: ThemeComponents; // ← Simple, non-generic type
  palette?: PaletteOptions;
  // ... other properties
}

export interface BaseTheme extends SystemTheme {
  mixins: Mixins;
  palette: Palette & (CssThemeVariables extends { enabled: true } ? CssVarsPalette : {});
  shadows: Shadows;
  transitions: Transitions;
  typography: TypographyVariants;
  zIndex: ZIndex;
  unstable_strictMode?: boolean;
}

export interface Theme extends BaseTheme, CssVarsProperties {
  cssVariables?: false;
  components?: ThemeComponents; // ← No generic, no circular reference
  unstable_sx: (props: SxProps<Theme>) => CSSObject;
  // ... other properties
}

2. Mirror exports (packages/mui-material/src/stylesOptimized/index.ts):

/**
 * This file must mirror the exports of `@mui/material/styles` for non-breaking changes in v7.
 * This entry point is an alternative for `@mui/material/styles` for optimizing TypeScript interface instantiation
 */

export {
  default as createTheme,
  ThemeOptions,
  Theme,
  // ... all other exports from @mui/material/styles
} from './createTheme';

3. Update original definition (packages/mui-material/src/styles/createThemeNoVars.d.ts):

// Before: Inline Theme definition (causes circular dependency)
export interface BaseTheme extends SystemTheme {
  mixins: Mixins;
  palette: Palette & (CssThemeVariables extends { enabled: true } ? CssVarsPalette : {});
  // ... 50+ lines
}

export interface Theme extends BaseTheme, CssVarsProperties {
  components?: Components<BaseTheme>;
  // ... 10+ lines
}

// After: Reference pre-defined Theme from stylesOptimized
import { Theme as ThemeOptimized } from '../stylesOptimized';

export interface Theme extends ThemeOptimized {
  components?: Components<Omit<ThemeOptimized, 'components'>>;
}

Why This Works

Breaks circular dependency:

  • stylesOptimized/createTheme.d.ts defines Theme with simple components?: ThemeComponents (no generics, no circular references)
  • styles/createThemeNoVars.d.ts extends ThemeOptimized instead of defining inline
  • TypeScript resolves ThemeOptimized once (from stylesOptimized), avoiding repeated instantiations

Non-breaking for v7:

  • Users continue using import { ThemeOptions } from '@mui/material/styles' as before
  • The original Theme interface still uses Components<T> generic for backward compatibility
  • Library authors can opt-in to @mui/material/stylesOptimized for better build performance

Performance impact (from analysis):

Before (with circular dependency):
  Instantiations: 744,661
  Memory used:    ~2,200MB (in Webpack builds)
  Build time:     High memory pressure, OOM failures

After (with stylesOptimized):
  Instantiations: ~300,000 (-60%)
  Memory used:    ~600MB (-73%)
  Build time:     Significantly reduced

Usage for Library Authors

To benefit from improved TypeScript performance, replace ALL imports from @mui/material/styles with @mui/material/stylesOptimized:

// Before: Using @mui/material/styles (causes memory spike)
import { createTheme, ThemeOptions } from '@mui/material/styles';

declare module '@mui/material/styles' {
  interface Theme {
    customProperty: string;
  }
  interface ThemeOptions {
    customProperty?: string;
  }
}

export const themeOptions: ThemeOptions = {
  /* ... */
};
// After: Using @mui/material/stylesOptimized (optimized performance)
import { createTheme, ThemeOptions } from '@mui/material/stylesOptimized';

declare module '@mui/material/stylesOptimized' {
  // ← Change module augmentation too!
  interface Theme {
    customProperty: string;
  }
  interface ThemeOptions {
    customProperty?: string;
  }
}

export const themeOptions: ThemeOptions = {
  /* ... */
};

Important:

  • This is an opt-in optimization - no breaking changes for existing code
  • Users continuing to import from @mui/material/styles will work but with higher memory usage
  • Library authors building design systems should migrate to stylesOptimized for CI/CD stability

Result

Metric Baseline (ThemeOptions) Fix Improvement
Instantiations 747348 337500 54.9%
Memory used 553 MB 364879K 34.0%
Check time 4.43s 2.38s 46.3%
Total time 5.65s 3.29s 41.7%

Before:

Files:              815
Lines:           164674
Identifiers:     130208
Symbols:         377356
Types:           117528
Instantiations:  747348
Memory used:    553221K
I/O read:         0.15s
I/O write:        0.00s
Parse time:       1.02s
Bind time:        0.20s
Check time:       4.43s
Emit time:        0.00s
Total time:       5.65s

After:

Files:              355
Lines:           139531
Identifiers:     110988
Symbols:         258356
Types:            92894
Instantiations:  337500
Memory used:    364879K
I/O read:         0.08s
I/O write:        0.00s
Parse time:       0.73s
Bind time:        0.18s
Check time:       2.38s
Emit time:        0.00s
Total time:       3.29s

@siriwatknp siriwatknp added typescript type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. package: material-ui Specific to Material UI. labels Oct 13, 2025
@mui-bot
Copy link

mui-bot commented Oct 13, 2025

Netlify deploy preview

https://deploy-preview-47069--material-ui.netlify.app/

Bundle size report

Bundle Parsed size Gzip size
@mui/material 0B(0.00%) 0B(0.00%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes

Generated by 🚫 dangerJS against 3307ce5

@zannager zannager added scope: system The system, the design tokens / styling foundations used across components. eg. @mui/system with MUI and removed package: material-ui Specific to Material UI. labels Oct 13, 2025
@siriwatknp siriwatknp force-pushed the fix/types-theme-components2 branch from b3d075e to e050e50 Compare October 14, 2025 05:52
@ZeeshanTamboli
Copy link
Member

@siriwatknp Nice approach! The solution looks good from what I’ve seen. Could we optimize the sx prop with theme too (SxProps<Theme>)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: system The system, the design tokens / styling foundations used across components. eg. @mui/system with MUI type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. typescript

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some bad ts performance cases

4 participants