Skip to content

Conversation

siriwatknp
Copy link
Member

@siriwatknp siriwatknp commented Oct 13, 2025

closes #42772, closes #47099

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

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 28befd2

@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>)?

@possum-enjoyer
Copy link

possum-enjoyer commented Oct 19, 2025

Ran the test and can confirm the numbers.
Also the ts lsp in vscode was faster again.

If you need some support with some chores around moving from styles to styles optimized and test stuff lmk :)

Edit: This fixes a very "weird" Problem with circular Depdencies regarding createTheme and Theme:

const theme1 = createTheme({
    components: {
        MuiButton: {
            styleOverrides: {
                root: {
                    backgroundColor: 'primary.main'
                }
            }
        }
    }
});

const theme2 = createTheme(theme1)

if you create a theme (theme1) with a ThemeOptions objects as the parameter and then pass the theme to another createTheme the compiler almost takes almost 3x as long. Its a weird problem because i would say no one "should" do that in real life bug due to how the ThemeOptions and Theme Types are constrcuted this is not an error and it produces a valid theme at the end. A way to circumvent this currently is to extract the ThemeOptions of theme1 and pass it to theme1 and theme2. If that is not possible in the code base, casting theme1 as ThemeOptions helps too:

const theme1Options: ThemeOptions = {
    components: {
        MuiButton: {
            styleOverrides: {
                root: {
                    backgroundColor: 'primary.main'
                }
            }
        }
    }
}
const theme1 = createTheme(theme1Options);

const theme2 = createTheme(theme1Options);

const theme3 = createTheme(theme1 as ThemeOptions)

I created an issue for the this behavior, to adress it separatley and would suggest to add a warning to the page.
Why do i highlight this issue in detail: It shows that this solution has the potential of dealing with many circular dependencies inside the createTheme / Theme / styles space

@siriwatknp
Copy link
Member Author

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

I would leave the SxProps out of this PR. I recalled that I did update the SxProps type to handle several cases. To optimized it, it will be a breaking change.

@siriwatknp
Copy link
Member Author

siriwatknp commented Oct 20, 2025

If you need some support with some chores around moving from styles to styles optimized and test stuff lmk :)

@possum-enjoyer please help test it out (best to test with any existing project to ensure compatibility)! it will help this PR to be merged faster.

@siriwatknp siriwatknp requested a review from Janpot October 20, 2025 03:08
@possum-enjoyer
Copy link

possum-enjoyer commented Oct 20, 2025

From my first Tests I found that some Components were missing a theme like CssBaseline and The x Packages (but I guess they will be updated later on). Everything else was compatible without any problems. No eslint or tsc errors and no refactoring of code was necessary besides changing the import and augmenting the ThemeCompoments type

Also for some reason if I split up my custom theme into smaller objects like

const MuiButton: ThemeComponents["MuiButton"] ={...};

Behaves weird (investigating the type of MuiButton via vscode always pointed me to the old Components type). But that could very much user error, I will have a second look.

The only thing that was a little bit "painful" was extending the ThemeComponents Type with many Components. It's 5 minutes at most when using a good ide.
On the other side I like augmenting the ThemeComponents type, it makes building a theme a little bit more like building a Lego set.

Besides that, the breaking of this circular dependency reduced tsc buid times drastically (more than 2.5x in one project). Great job finding that problem!

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.

Passing a Theme object into createTheme increases compile time Some bad ts performance cases

5 participants