Photo by eshaunessy

Burn the Barrel!

Brett Uglow

--

TL;DR: avoid using barrel files — it could improve your app’s runtime, build-time and test-time performance.

Background 🏞️

A few months ago, I came across a series of articles by Steven Lemon to do with improving Jest performance. For the uninitiated, Jest is a JavaScript (JS) testing framework — it allows you to write unit-tests for your JS codes (including for UI component codes). Jest has been around for 7+ years, but it’s more performant than Vitest (despite their marketing).

The reason I was researching Jest-performance is that I’m part of a team that is building a brand new NextJS-based website, and our GitHub-action workflow was spending a lot of it’s time running our unit-tests. If we could speed up the tests, we can save time & money & improve the dev-experience (DX).

In Steven’s article Why is My Jest Test Suite So Slow?, among some of his other interesting findings is a discovery that barrel-files are a major reason for slow tests.

What is a barrel file? 🛢️📁

In JS, a barrel file is a module that re-exports a bunch of exports from other places:

// some-package/index.js
export * from './a/Accordion';
export * from './b/Badge';
export * from './c/Container';
...

This allows consumers of a package to write (for example):

import { Accordion } from 'some-package'; , instead of:

import { Accordion } from 'some-package/a/Accordion' .

Another benefit of barrel files as they control the external interface to a package so that the consumer doesn’t need to care how some-package is structured internally. Most libraries usually have a barrel file in the root directory containing everything they want to expose to the consumer.

So barrel-files are good because:

  • Consumers can type-less when importing something
  • Consumers can be blissfully ignorant about how a package is internally-structured.

But are there any down-sides to using barrel files? From Steven’s article:

“The problem is that Jest has no idea where the component we’re importing is located. The barrel file has intentionally obfuscated that fact. So when Jest hits a barrel file, it must load every export referenced inside it. This behavior quickly gets out of hand for large libraries like @mui/material.”

Our app wasn’t using a library as-large-as Material-UI, but it was using an internal library that had over 200 React components, which was growing each week. So I decided to test whether Steven’s finding’s applied in our use-case as well…

The Results 🏆

I wasn’t 100% sure what was going to happen. But given the detailed (and hopefully-truthful) analysis I had read, I was quietly hopeful that there would be an improvement.

Methodology

To try to get an apple-to-apple comparison, I measured the unit-test times in 2 scenarios with the same computer:

  1. Time taken to run unit-tests with an empty cache
  2. Time taken to run unit-tests with a populated cache
  3. Do the above tests with the before-code, then with the after (barrel-less) code.

The numbers below are averages from 5 test-runs for each scenario.

It’s worth noting that the computer I’m using is a brand new 16" MBP with M2 Pro CPU and 16GB — courtesy of my employer Mantel Group (it’s a great place to work). So this is best-case scenario. On Github, where our CI/CD workflow runs, these same tests take about 10x longer. So a 5% increase in performance is significant to us.

Bonus Benefit 🎁

There were 2 additional, unexpected, benefits to removing the barrel files:

  1. Our NextJS app’s page sizes (HTML + JS) decreased by between 5–10%. This has a material-benefit on the website’s user-experience.
  2. The build time on CI for the NextJS app decreased by about 25% (over 4 minutes to around 3 minutes — more data required).

Sample of page size improvement:

Small sample of page size differences when doing a NextJS production build (next export). Note that the First Load JS file is incuded by every page; an improvement here is a massive win for the whole site.

The hard part — converting the code

Converting the code from using barrel-files to not-using-barrel-files took a few hours one weekend. About 300 files were changed. The dependent-package’s package.json file’s exportswent from this:

"exports": {
".": {
"import": {
"types": "./dist/src/index.d.ts",
"default": "./dist/src/index.js"
}
},
"./*": {
"import": {
"types": "./dist/src/*.d.ts",
"default": "./dist/src/*.js"
}
}
}

…to this:

"exports": {
"./*": {
"import": {
"types": "./dist/src/*.d.ts",
"default": "./dist/src/*.js"
}
}
},

All of the barrel files were removed too, to prevent accidental regression.

Summary

I love consuming barrel files, but their convenience comes with a performance price-tag, especially in NextJS static-apps. For smaller projects, you might not notice (or care). But for larger projects, consider whether the convenience is worth the increase in build-times, test-times and run times (page sizes).

Other barrel-file articles

Update 6-Mar-2023

Apparently there’s a property called "sideEffects": false that can be specified in a package.json file, to tell Webpack/Terser that your package can be safely tree-shaken. I gave it a try on the codebase, and this is what I found:

  • 208kB common JS → 207kB
  • No impact on test times

Apparently "sideEffects" is not a silver-bullet. It depends. It might work for your use case, so give it a try before deciding to remove barrel files completely.

Thanks to Wenny Hidayat for the review!

--

--

Brett Uglow
Brett Uglow

Written by Brett Uglow

Life = Faith + Family + Others + Work + Fun, Work = Software engineering + UX + teamwork + learning, Fun = travelling + hiking + games + …

Responses (3)