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)
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 testB
, in which these imports are replaced by named imports. Here are the results:A
B
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
innode_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:
Step 2
Count and log loaded modules. code pointer 1 Add before the marked line:
code pointer 2 Add before the marked line:
Outcome
After patching the
Runtime
class I got the following numbers for my simple unit tests:A
B
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
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 toesbuild
output that enables things likeistanbul 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 leastesbuild
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.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 allhttps://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:
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
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
āļøbabel-plugin-direct-import. Adding
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:
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 tojest.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 secIn 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
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 thatjest.mock
is overriding theimport
which doesnāt exist anymoreā¦@TSMMark
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:
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:
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)
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!