jest: Slow start times due to use of barrel files

šŸ› Bug Report

It is unclear that large dependency graphs caused by the use of barrel files can slow test initialization dramatically. There are some tangential mentions of this in the documentation, but it is not outlined in a clear and direct manner. This likely leads to a lot of inefficiency in many projects.

Here are the most important points to stress:

  • Jest simulates Nodeā€™s require cache to allow for isolation and mocking
  • The require cache is built independently for every test suite
  • All dependencies of a file are included, even if unused

It is the last bullet that leads to the largest reduction in efficiency, due mainly to barrel files. Barrel files are index files that re-export the exports of other files in a directory. They make it possible to import multiple related dependencies in a single import statement. The downside of this is that Jest sees all of the re-exported contents of the barrel file as dependencies and crawls through them, used or not.

Reducing the use of barrel files can help quite a bit in reducing the amount of time it takes before a test can start. This is especially true of dependencies pulled from NPM packages. Packages are typically developed with a root index file that acts as a barrel file for the entire package. If the package is rolled up into a single file at build time, there is only one file for the Jest runtime to open and parse. If the package publishes its source files independently, without a rollup stage, the Jest runtime will need to open and parse every file in the package independently.

In an enterprise setting, there are often internally developed tools and libraries. These packages can grow to be fairly large, and given their internal use, it can be tempting to provide the source files as the output of the packages. In fact, this can improve tree shaking when building the applications that depend on them. Jest, however, can suffer greatly in this environment because the raw number of files can grow without bounds. This problem becomes exponentially worse when overly tight internal dependency semvers reduce the ability of package managers to de-duplicate their installs.

Resolving these issues can lead to tremendous decreases in Jestā€™s test suite initialization and this should be highlighted in the documentation. Barrel files can have a huge impact on the number of files that the Jest runtime needs to parse, and that is not clear without a deep dive into the way dependencies are evaluated. Sharing this knowledge more broadly could make an already fantastic test runner that much better and improve the quality of many products that rely upon it in the process.

To Reproduce

As this is a documentation issue and not a code issue, this may not apply. However, in the spirit of completeness:

  • Read the Jest documentation
  • Experience slow test start times in enterprise scale development
  • Be unaware of the impact that barrel files can have on running Jest

Expected behavior

The expectation is that the Jest documentation should more explicitly explain the impact that barrel files can have on test start times.

Link to repl or repo (highly encouraged)

Given that this issue is manifested in large scale applications with many inter-related dependencies, it is not feasible to provide a replication of the issue.

envinfo

N/A

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 104
  • Comments: 38 (1 by maintainers)

Most upvoted comments

We have around 1300 test suites with about 6k tests. Jest is taking a lot of time. We believe Barrel import is one of the main contributors to the overall slowness. It would be super helpful to get some guidance from the Jest maintainers on addressing this problem.

We also face long startup times in our tests. After trying a lot of ā€œsolutionsā€ mentioned on the internet we discovered that in our case the problem are the barrel files.

I created two simple unit tests in our code base. Test A which imports from barrel files and test B, in which these imports are replaced by named imports. Here are the results:

test duration
A ~29s
B 91ms

To be able to dig deeper into the problem you have to understand what is going on during the long startup time. For this I modified the class Runtime in node_modules/jest-runtime/build/index.js.

Modify Jest Runtime Class

Instead of a patch I link the code-pointers and the added code, in case you run a different jest version. All modifications are marked with // MARKER to easily find them afterwards.

The file to modify is: node_modules/jest-runtime/build/index.js

Step 1

Add additional class properties to track the number of loaded modules. code pointer Add before the marked line:

// MARKER
this._LOAD_COUNTER = 0;
this._INDENTS = [];
// MARKER

Step 2

Count and log loaded modules. code pointer 1 Add before the marked line:

// MARKER
this._LOAD_COUNTER += 1;
console.log(`${this._INDENTS.join('')}>>_loadModule`, this._LOAD_COUNTER, moduleName)
this._INDENTS.push('\t')
// MARKER

code pointer 2 Add before the marked line:

// MARKER
this._INDENTS.pop()
// MARKER

Outcome

After patching the Runtime class I got the following numbers for my simple unit tests:

test loaded modules
A 17424
B 1503

And you can clearly see by the logged output that jest is importing everything in case of barrel files before running a test.

After adjusting only some of the imports (to use named imports) in our code base as a PoC, I was able to reduce the build time from 15 minutes to 6 minutes.

I have made a simple repo that reproduces this issue using a barrel file provided by a large third-party library, in this case Material UI icons: https://github.com/rsslldnphy/jest-barrel-files

On my machine, the test that imports icons as import * as Icons from "@mui/icons-material"; takes about 7 seconds to run, compared to fractions of a second for the test that doesnā€™t.

I do not have a good understanding of the mechanics of what goes on (with treeshaking etc) when importing files in this way, but Iā€™m not encountering this slowness issue the other tools Iā€™m using - is there a way to make jest aware of code that can be ignored?

@fadi-george sorry I donā€™t have the script anymore (Maybe I can look it up in the git history), but Iā€™ve found even faster solution. Iā€™m using esbuild to bundle ALL my tests + code into one huge file, then I run jest on this file only. so instead of 350sec. with jest as it is, itā€™s taking 30sec tops.

I had to do some juggling to make esbuild and jest work together, like importing svgs etc. you can see the script here

is there a plugin or something that can solve all barrel files usages?

Unfortunately not. There are several workarounds in this thread, but none are low-effort.


FYI, if youā€™re looking at any of the above issues, here are some limitations and things to experiment with:

- Remove barrel files and only test what you can see

Sure-fire way to fix this. However, not feasible in larger codebases using barrel files liberally. (Plus, itā€™s tilting at windmills. Build for the actual need, not the ideal.) In addition, mock things your test subject is using, so that your test is focused on exactly the logic you wrote. Let TypeScript make sure your interfaces continue to align, and if youā€™re worried, you can have another layer of tests to ensure interop between modules. (We have a test capital-i, where we have 100% unit test coverage, and a comprehensive set of end-to-end tests that validate high-level module interop. Almost no integration tests because those often end up slower than our end-to-end tests!).

- Try out the various Babel plugins listed above

This may work for you on relatively small projects without using a lot of mocks and spies. However, most of these are opt-in, and are only useful if you can identify up-front where a lot of your heftier barrel files are. In many cases, this isnā€™t feasible.

- Try another test runner

Something like Vite might work better, but most people wonā€™t have a 1:1 transition from Jest. Vite uses enforces strict modules (e.g., not mockable/spyable in the same ways Jest is). Also, it suffers from the same issues as those described below, since it also uses ESBuild. But you might have luck here if you donā€™t do anything fancy with mocks/spies.

- ESBuild/SWC all the things

If you barely use Jestā€™s mocking/spy features, this might be feasible. However, any alternative transformer to Babel will be a pain if you use them. Synthetic exports in babel are loosely constructed (e.g., theyā€™re mutable references, so can be replaced spied on). Tools like SWC and ESBuild adhere more to official specs for modules (e.g., modules are immutable, canā€™t be monkey patched, etc.).

I have not yet tried Rollup for our tests. Might be worth a shot if someone has the energy, but Iā€™d be surprised if itā€™s any faster/better/useable than any of the above examples.

I have experimented with a custom Jest transformer, too, that uses Babel to hoist jest.mock calls; esbuild to transpile everything else; and comprehensive set of modifications to esbuild output that enables things like istanbul ignore lines (esbuild transpiles out all comments unless you mark them as legally required) and switches it to a loose module system with easy find/replace. But this also has limitations (e.g., if youā€™re not careful, it can break your source maps; it still transpiles a million things, but at least esbuild is faster than babel at it). This is a fraught game of whack a mole. You have to know and account for every weird way people use mocks and spies ahead of time, and with thousands of tests, there are probably a couple hundred combinations to account for šŸ˜­


In my case, weā€™re a bit stuck. We have a huge codebase with tons of mocks and spies (we require 100% code coverage, and suggest that developers have 1:1 test -> adjacent test file with 100% coverage, so our codebase is at least half test code). This has been excellent for maintaining high code quality based on the standards when we began (circa 2019). But the barrel file issue is a thorny one. In new code, we donā€™t allow them, but weā€™ve got thousands of tests that drag because of this.

To put it simply: thereā€™s no easy solution here. In hindsight, it was probably engineering malpractice to suggest the use of barrel files, and weā€™re paying for our past oversight! šŸ˜†

The only clear way forward at the moment is to suck it up, deal with slow tests for a while, and refactor them as you get the opportunity. Iā€™ve been experimenting on the side for my team for years looking for ways to make this better, especially as our lazier engineers realize how easy weā€™ve made it to write thick tests that exercise huge swaths of the platform. Yeah, itā€™s less test code, but it takes two minutes to get feedback. Some folks donā€™t mind waiting, but I sure do.

Despite our best efforts, even the best engineers will take shortcuts to shave off a few minutes from their tickets, and make everyone pay for that in the time it takes to run tests. Amortized over several years and several dozen developers, and youā€™re looking at 3 minute startup times for a single, 3ms test that ensures an if statement works correctly in a 12 line file.

I almost think Jest (or some other tool) needs a way to elevate how much code a certain test is using and give us the ability to set upper limits. Itā€™s a waste of time and CPU to load 20,000 modules to test a couple of logical branches. No matter what Jest does to make this better, without the automatic tooling for us tech leads to set upper bounds on this, I donā€™t foresee this being all that fixable with some out of the box solutionā€¦ šŸ¤”

I have taken a stab at writing my own jest transformer to accomplish this. Iā€™m using an Nx mono repo with about 11 Angular libraries inside (v17.1), I have about 1600 unit tests, and it takes about 5 minutes to run. The jest transformer Iā€™ve created will correctly replace all of the paths in my ts config with the import pointing to the actual file of where itā€™s exported. However, my solution doesnā€™t work when it comes to transforming 3rd-party paths like @angular/core, @ngrx/store, etc., because ts.createProgram will only return declaration files for any path I pass in that points to a node_module file. With this transformer only changing local imports, itā€™s only shaved about 30 seconds off the execution time.

If there is anyone who might be able to lend a helping hand with this transformer, I would be very appreciative. Iā€™m sure it could be very beneficial to others coming here as well. I only started working with the TypeScript compiler api a few days ago, so Iā€™m sure that my solution is far from optimal. modifyImportDeclaration is where I would need the most help. I was also looking into SWC, to see if they had an api that would do something like ts.createProgram and ts.transpileModule, but I couldnā€™t find anything.

// jest.preset.js 

{
  ...
  transform: {
    '^.+\\.(ts|mjs|js|html)$': [
      '<rootDir>/jest-import-transformer.js'
    ]
  },
  ...
}
// jest-import-transformer.js

var fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
var angularPreset = require('jest-preset-angular');
const presetConfig = {
  // NOTE: All tsconfig.spec.json files used have their target set to ES2016 to deal with the following problem
  // https://github.com/nrwl/nx/issues/15390#issuecomment-1457792176
  tsconfig: '<rootDir>/tsconfig.spec.json',
  stringifyContentPathRegex: '\\.(html|svg)$',
  isolatedModules: true
};
var baseTransformer = angularPreset.default.createTransformer(presetConfig);
const ts = require('typescript');
const tsConfig = require('./tsconfig.base.json');
const pathCache = {};
const filePaths = {};

function extractAllExportStatements(filePath) {
  const program = ts.createProgram([filePath], {
    target: tsConfig.compilerOptions.target,
    module: tsConfig.compilerOptions.module
  });
  const output = {};
  for (const sourceFile of program.getSourceFiles()) {
    if (!sourceFile.isDeclarationFile) {
      // Walk the tree to search for exported nodes
      if (!filePaths[sourceFile.fileName]) {
        filePaths[sourceFile.fileName] = path
          .relative(__dirname, sourceFile.fileName)
          .replaceAll('\\', '/');
      }
      const fileName = filePaths[sourceFile.fileName];
      ts.forEachChild(sourceFile, node => visit(node, fileName));
    }
  }

  return output;

  function visit(node, fileName) {
    // Only consider exported nodes
    if (!isNodeExported(node)) {
      return;
    }

    if (ts.isModuleDeclaration(node)) {
      // This is a namespace, visit its children
      ts.forEachChild(node, visit);
    } else if (node.name) {
      output[node.name.text] = fileName;
    } else if (ts.isVariableStatement(node)) {
      output[node.declarationList?.declarations[0]?.name?.text] = fileName;
    } else if (ts.isExportDeclaration(node)) {
      output[node.exportClause?.name?.text] = fileName;
    } else {
      debugger;
    }
  }

  function isNodeExported(node) {
    return (
      (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) !== 0 ||
      (!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile) ||
      node.exportClause?.name?.text // export * as [Name] syntax
    );
  }
}

function ensureBarrelIsCached(oldPath, barrelPath, options) {
  if (!pathCache[barrelPath]) {
    // Not in the cache, let's check the cacheDirectory
    const cacheFilePath = path.join(options.config.cacheDirectory, oldPath + '.json');
    if (fs.existsSync(cacheFilePath)) {
      pathCache[barrelPath] = JSON.parse(fs.readFileSync(cacheFilePath));
    } else {
      // https://github.com/microsoft/TypeScript/wiki/Using-the-Compiler-API#using-the-type-checker
      pathCache[barrelPath] = extractAllExportStatements(barrelPath);
      const dirName = path.dirname(cacheFilePath);
      mkdirp.sync(dirName);
      fs.writeFileSync(cacheFilePath, JSON.stringify(pathCache[barrelPath]));
    }
  }

  return pathCache[barrelPath];
}

function invalidateAndReloadBarrelCache(oldPath, barrelPath, options) {
  const cacheFilePath = path.join(options.config.cacheDirectory, oldPath + '.json');
  if (fs.existsSync(cacheFilePath)) {
    fs.rmSync(cacheFilePath, { force: true, recursive: true });
  }
  pathCache[barrelPath] = null;
  return ensureBarrelIsCached(oldPath, barrelPath, options);
}

function modifyImportDeclaration(node, src, fileName, options) {
  const oldPath = node.moduleSpecifier.text;
  // only transform paths defined in our tsConfig
  const barrelPath = tsConfig.compilerOptions.paths[oldPath]?.[0];
  let barrelExports;
  if (barrelPath) {
    barrelExports = ensureBarrelIsCached(oldPath, barrelPath, options);
  }

  if (!barrelExports) {
    return src;
  }

  if (node.importClause && node.importClause.namedBindings) {
    var directImports = [];
    for (importNode of node.importClause.namedBindings.elements) {
      importNode = importNode.getFullText().trim();
      if (!barrelExports[importNode]) {
        barrelExports = invalidateAndReloadBarrelCache(oldPath, barrelPath, options);
        if (!barrelExports[importNode]) {
          console.warn('Import not found!', importNode, fileName);
          break;
        }
      }

      var relPath = path
        .relative(fileName, __dirname + '/' + barrelExports[importNode])
        .replaceAll('\\', '/')
        .replace('.ts', '')
        .replace('../', '');
      directImports.push(`import {${importNode}} from '${relPath}';`);
    }
    if (directImports.length) {
      var transformedFileSrc = src.replace(node.getFullText(), directImports.join('\r\n'));
      return transformedFileSrc;
    }
  }
  return src;
}

function processFile(src, filename, options) {
  var transformedFileContent = src;
  ts.transpileModule(src, {
    compilerOptions: tsConfig.compilerOptions,
    fileName: filename,
    transformers: {
      before: [
        context => {
          return sourceFile => {
            function visit(node) {
              if (ts.isImportDeclaration(node)) {
                transformedFileContent = modifyImportDeclaration(
                  node,
                  transformedFileContent,
                  filename,
                  options
                );
                // TODO find a way to replace the node with multiple import nodes instead
              }
              return ts.visitEachChild(node, visit, context);
            }
            return ts.visitNode(sourceFile, visit);
          };
        }
      ]
    }
  });
  return transformedFileContent;
}

function hasDebuggerAttached() {
  // We don't want to run when the debugger is attached, because it would throw off sourcemaps
  return !!process.env.VSCODE_INSPECTOR_OPTIONS;
}

module.exports = {
  process(src, fileName, options) {
    let newSrc = src;
    if (
      !hasDebuggerAttached() &&
      fileName.endsWith('.ts') &&
      !ignoreFiles.some(f => fileName.includes(f))
    ) {
      newSrc = processFile(src, fileName, options);
    }
    return baseTransformer.process(newSrc, fileName, {
      ...options,
      transformConfig: presetConfig
    });
  }
};

const ignoreFiles = ['test-setup', 'test-polyfills'];

Here is a first version of the plugin I mentioned above (as gtsop-d). It is tested on our CRA app (React 16, jest 27) and roughly gives a 50% speed boost. It is quite alpha but I hope it can help you, feel free to post any issues there to get it working better on your codebases. Ideally you can start using it without modifying your codebase at all

https://www.npmjs.com/package/@gtsopanoglou/babel-jest-boost

Edit: If youā€™ve used the plugin above successfully, or have trouble using it, please open an issue and iā€™ll try to help you. I intend for it be an actual solution to this problem we are discussing here, not just a tailor-made solution for my codebase. Thanks šŸ™šŸ»

TLDR

For those struggling with 3rd party barrle imports and using babel as a jest transformer - babel-plugin-direct-import does seem to improve load times (at least for us)

We test a component which imports another component from a 3rd party lib (letā€™s call it libA to be short). The libA imports an icon from some icon pack which exports icons via a barrel import, kinda similar to the way material-ui does it. So what we have as a result is jest importing and transforming around 8k icons even though libA just uses a single one of them. I had to exclude this icon pack from transformIgnorePatterns because I simply could not figure out how to foce jest into working with esm, ā€“experimental-vm-modules did not work for me, I guess there is some problem with the way that icons pack build commonjs and esm files

On my PC such a test was running for 80-90 sec (the actual test took around 50ms) I donā€™t have a solid grasp on how all these modules magic works so this is what I tried:

  1. babel-jest -> @swc/jest with no .swcrc at all. My test started to take only 35-40 sec to run which already was a huge improvement. I tried to configure swc with @swc/plugin-transform-imports to get rid of barrel imports but it seems like I cannot use look-around regexp in Rust to split an icon component name the way I need it (( Looking into it now

  2. jest -> vitest. I had huge hopes for that one but unfortunately it didnā€™t work with icons pack, I get a <icons pack name>/<icon>.js seems to be an ES Module but shipped in a CommonJS package. Again - I guess the icon pack is doing smth wrong while building esm

  3. āœ”ļøbabel-plugin-direct-import. Adding

  plugins: [
    [
      "babel-plugin-direct-import",
      {
        modules: ["<icon pack name>"],
      },
    ],
  ],

made our test run for 10 sec comparing to initial 80-90 so I see it as a huge improvement

Hope it helps someone! Cheers!

We decided to introduce jest.mock(ā€˜your-moduleā€™) to our jest setup file and we are able to see a decrease in setup time.

Reference: https://jestjs.io/docs/manual-mocks#mocking-user-modules

@rsslldnphy for material specifically, which is a quite big library, I isolate the parts of the library my code actually uses in a re-exported barrel file:

// components/index.ts

export { default as AppBar, type AppBarProps } from "@mui/material/AppBar";
export { default as Button, type ButtonProps } from "@mui/material/Button";
// ...

export { default as DateRangeIcon } from "@mui/icons-material/DateRange";
export { default as CheckCircleIcon } from "@mui/icons-material/CheckCircle";
export { default as LocalShippingIcon } from "@mui/icons-material/LocalShipping";
// ... keep in mind that even 50-100 icons is only a small part of the 2000+ icons material exports

To be fair, we did this to mark to other developers in the team what parts of the material ui they could use without having to have a talk with our UI/UX designer beforehand. But it also mitigates this issue as a side effect.

@mikerentmeister I have also embarked on this endeavor myself.

I have been building a babel plugin that replaces all the import statements to point directly to the original exporter. I have also modified the mock calls so that it applies to jest.mock calls as well. Our codebase is a quite large CRA with around 500 test files. It takes ~700 sec to run without this plugin. With the plugin it runs in around ~330 sec

In order for my plugin to be practical I need to modify the parser Iā€™m spawning so that it dynamically uses the jest/babel configuration of the project. As of right now I have sort of hardcoded my projectā€™s config within my plugin

Would #9430 resolve this issue by allowing for tree-shaking in Jest tests?

As far as I know, tree shaking is a bundler feature, not a native ESM feature, so itā€™s dubious it would help.

Just a thought on this as recently i was facing a similar issue and the way i fixed it is by creating a custom barrel import transformer. The way it works is in first step we iterate all files to determine all the exports of files in project making a Map of import to lookup later.

Now when the test start to execute, using jest transform configuration, then execute a transform which uses the import lookup map created in first step to rewrite the import statements to specific imports statements and then jest executes on the transformed code.

Was able to significant improvement with this approach when using jest with ts-jest (isolatedModules enabled).

I donā€™t use jest.mock at all (And I think that itā€™s bad practice in most cases). I also donā€™t think that itā€™s possible to use when doing the esbuild bundle. The bundle already includes all the code, and I think that jest.mock is overriding the import which doesnā€™t exist anymoreā€¦

@TSMMark

Iā€™ve got it printing out the results, but Iā€™m not sure what to look for.

To investigate, you should select a simple and rather small test that has an unexpectedly long start time.

Maybe you can see the difference better in the following react example. As mentioned by others @mui/icons-material is a good candidate to slow down your tests massively.

If you are not already using @mui/icons-material in your project, add it as a devDependency for demo purposes.

Create a test tsx-file and add the following code:

import React from 'react'
// this import slows your test down
import { DoNotDisturbOn } from '@mui/icons-material'
// this import not
// import DoNotDisturbOn from '@mui/icons-material/DoNotDisturbOn'

test('should demo the problem', () => {
  <DoNotDisturbOn />
  expect(1).toBe(1)
})

The test with the second import for DoNotDisturbOn should start much faster because it imports far fewer modules.

Now run the single test with only one of the two imports for DoNotDisturbOn and compare the amount of loaded modules. Search in the output for @mui/icons-material to see what jest also imported from it.

With this knowledge, you should be able to find problematic imports in your codebase.

We have around 3200 test suites and 35k tests. We had the similar issue. @mui/icons-material is the culprit in our case. We simply mocked the package and time came down from 50 minutes to 18 minutes. We are still going through other packages but thatā€™s a significant time improvement for mocking just one package, Havenā€™t tried with babel-plugin-direct-import. But iā€™m concerned how modules array will look in larger projects which uses around 50 MUI icons.

For the one looking for a quick solution, adding the option isolatedModules: true to ts-jest and it divided by 6 the execution time. We are now able to run parralel agent on a pipeline to run our thousands of tests thanks to this little option.

@boubou158 added a typescript based sample here with some readme- https://github.com/dsmalik/ts-barrel-import-transformer

Not sure if it will help, but hereā€™s my 2 cents:

I face problems with barrel files not only in jest, but on some external and internal libs that have optional peer dependencies. Including these libs which do all exports from a barrel causes compile errors due to indirect import of unused components that use optional not installed dependencies

To fix this, I was using the already mentioned babel-plugin-transform-imports, which IMO works great (although I had to fork it and fix import aliases issues)

I even tried to improve the transform imports solution writing babel-plugin-resolve-barrel-files, but its a simple solution for ESM modules only.

But I guess that for jest, people could try implementing a lazy import with mocks:

jest.mock('module-with-barrel', () => {

  const RequiredComponent = jest.requireActual('module-with-barrel/RequiredComponent').default;
  return {
     __esModule: true,
    RequiredComponent
    // or use a more lazy approach...
    //  get RequiredComponent() {
    //     return jest.requireActual('module-with-barrel/RequiredComponent').default;
    // }
   }
})

Unfortunately, it seems that transpilers (TypeScript), bundlers (WebPack, SnowPack, Vite, Rollup) canā€™t differentiate exactly which is the import youā€™re trying to use

They probably can, but since barrel files are normal source code files, they can execute side effects (read the tip) (eg: some dep exporting global stuff)ā€¦ so itā€™s more safe to just donā€™t optimize unless you explicit tell them to do it (the case for the babel plugins and webpack configs)

@rsslldnphy for material specifically, which is a quite big library, I isolate the parts of the library my code actually uses in a re-exported barrel file:

// components/index.ts

export { default as AppBar, type AppBarProps } from "@mui/material/AppBar";
export { default as Button, type ButtonProps } from "@mui/material/Button";
// ...

export { default as DateRangeIcon } from "@mui/icons-material/DateRange";
export { default as CheckCircleIcon } from "@mui/icons-material/CheckCircle";
export { default as LocalShippingIcon } from "@mui/icons-material/LocalShipping";
// ... keep in mind that even 50-100 icons is only a small part of the 2000+ icons material exports

To be fair, we did this to mark to other developers in the team what parts of the material ui they could use without having to have a talk with our UI/UX designer beforehand. But it also mitigates this issue as a side effect.

Nice idea on both counts @PupoSDC! May well implement this. Will be a bit of a faff to set up and maintain with icons especially but not a bad trade-off at all for mitigating this issue. Thanks!