Mui bundle size?


I have a problem with mui lib that is taking almost 2.5MB of my bundle size

I only have named import like this import X from "@mui/material/X" and also setup in my .babelrc file

        "modules": ["@mui/material", "@mui/icons-material"]

I do not know what I am missing ?


You shouldn’t need to do both (direct import plugin and named imports directly manually). Also I usually use this plugin instead:

  "plugins": [
        "@fortawesome/free-solid-svg-icons": {
          "transform": "@fortawesome/free-solid-svg-icons/${member}",
          "skipDefaultConversion": true
        "@fortawesome/free-regular-svg-icons": {
          "transform": "@fortawesome/free-regular-svg-icons/${member}",
          "skipDefaultConversion": true
        "@fortawesome/free-brands-svg-icons": {
          "transform": "@fortawesome/free-brands-svg-icons/${member}",
          "skipDefaultConversion": true

Remember that you need to install the plugin as well and make sure the conversion is really happening by looking to your final code inside the bundle.

Thanks @filipenevola, I must have another problem then because I do not have any undirect import in my code and still have this huge bundle.
Very weird, I have try removing dependencies that might cause problems but I still have the same size

I think this is still a problem in Meteor

Installing MUI as a local NPM and commenting the exports of all components I don’t use shaved off 200kb from the @mui/material library. I couldn’t find any other way to do it.

I’m curious about this too.

I import my @mui packages like this:

 import Typography from '@mui/material/Typography';

…rather than like this:

 import {Typography} from '@mui/material;

I believe I’ve read that, in the absence of tree-shaking, using the second approach imports all of mui.

It seems to help. I’m showing @mui as taking up 909kB.

OTOH, I’m importing debounce like this:

 import {debounce} from "@mui/material";

…because there doesn’t seem to be any such file as “@mui/material/debounce”;

So I don’t know why that wouldn’t be importing all of mui.

1 Like

I’ve been through that and imported everything as a direct import. I even used the babel plugins recommended on the MUI website. I just couldn’t get rid of the unused components. This seems to be a problem in Webpack too as I’ve seen in many posts.
I managed to take @mui/material down from 570kb to 360kb.
Also these need to be imported from their right folders:

import { ThemeProvider, useTheme } from '@mui/material/styles'
import useMediaQuery from '@mui/material/useMediaQuery'
1 Like
import { debounce } from '@mui/material/utils'
1 Like

If it’s only the initial bundle size you care about then you might want to use dynamic imports. Minimal repro with a fresh meteor project:

$ meteor create muitest
$ cd muitest
$ npm install @mui/material @emotion/react @emotion/styled
$ meteor

Create component loader in client/useComponent.jsx:

import React, {useEffect, useRef, useState} from 'react'

export const useComponent = (fn, onError = console.error) => {
  const proxy = useRef(ProxyComponent)
  const ref = useRef(ProxyComponent)
  const [loaded, setLoaded] = useState(false)

  useEffect(() => {
      .then(component => {
        ref.current = component
  }, [fn])

  return loaded
    ? ref.current
    : proxy.current

const ProxyComponent = () => (<span>loading...</span>)

And then in hello.jsx:

import React, { useState } from 'react';
import {useComponent} from '../../client/useComponent'

const asyncTimeout = ms => new Promise((r) => setTimeout(() => r(), ms))

export const Hello = () => {
  const [counter, setCounter] = useState(0);
  const Button = useComponent(async () => {
    const Button = (await import('@mui/material/Button')).default
    await asyncTimeout(500)
    return Button

  const increment = () => {
    setCounter(counter + 1);

  return (
      <Button variant="contained" onClick={increment}>Click Me</Button>
      <p>You've pressed the button {counter} times.</p>

Using visualizer I get around 10KB initial mui size:

I know this is just a proof of concept but something you might leverage for further optimization

Edit: for better scaling you might wrap common component imports as exported fn:

export const loadButton = async () => {
    return (await import('@mui/material/Button')).default

It’s some effort but might be worth in the end


That’s a very interesting dynamic component load. The problem with MUI is that it has so many bits and pieces, not sure it would be feasible to fetch them all through a function (if we would just build the fetching system to receive the component name as a parameter in a function)

There is another way, to avoid MUI down the code until after the router and load all routes dynamically and so MUI would be fetched dynamically within a route component but …

My “bundle strategy” is to put as much as possible and as little as possible into the initial bundle. Projects in early development tend to have a lot of code updates and when you get the user volumes you probably want to serve them, bandwidth wise, from the CDN rather than pushing code from your Node servers.

Ideally: CDN → Local cacheing with service worker → consume.

Let’s say, you have a 200kb that you need to give to the user in the form of MUI. You can provid it split (from Node), full MUI (from Node) or full MUI (from CDN) within your Meteor bundle.

When traffic increases 200kb easily turns into 10GB and while dynamic imports are “local”, you get them from where the Node servers are, CDN-ing puts the edge near the user globally and not only the speed is far better but the cost is much lower. Serving regional CDN is much cheaper than EC2 bandwidth (in the case of AWS).

Then if you SSR thousands of pages and deliver gigs of dynamic imports to new visits you end up spinning more and more cloud servers.

Ok … this turns into a novel now :))) . I think for the case of MUI I personally just want to remove the “junk” that comes with it in terms of useless components and theme presets and in general I am looking to get more and more NPM (or code in general) from private CDNs.

Code splitting is great but until we CDN those mini bundles the cost of this performance gain is too hign IMHO.


That’s a great explanation and already a little a guide here. I think it all depends on expected target audience size. @storyteller might an interesting input for a scaling guide


@jkuester @storyteller @filipenevola this was somehow forgotten in the history of Meteor.
The conversation includes relevant aspects such as:

  1. Security: unsafe eval due to Dynamic Imports
  2. CDN-ing
  3. Removing the dependency on the dynamic-import package from 2 other packages so that dynamic-import may be made optional.

@paulishca I took on this one and opened a pull request: Fix: remove dynamic import as hard dependency on packages by jankapunkt · Pull Request #12879 · meteor/meteor · GitHub However, I think I will need some help as I don’t fully understand why the tests fail :see_no_evil:

1 Like

I see in Test Group 0 in Build and Test : Cannot find package "inter-process-messaging".
Recommendation is to … try again later :).

I saw that but I don’t know why it happened as I never touched this package nor did I find any relations of it to the dynamic import or usage of such.

Yes, I would bet on resuming this PR after merging Meteor 3 to develop branch.

At the time, this PR was really close, so it’s worth investing in the final solution for production bundles.

If we focus on having tree-shaking only for production, the work left shouldn’t be that hard or long.

Now, if we want to use tree-shaking in development, then it’s another topic, as this code will slow down the development process with Meteor even more.

Do you agree, or do you see another way of achieving what you need?


I think tree-shaking only makes sense for production.