Posts

Why you should stop using index files

If you are a front-end developer, you probably often use index files, and you probably find them very useful.

However, did you know that they have significant drawbacks, especially regarding performance?

What are index files?

Index files have been introduced by Node.js import mechanism, i.e. require, that will look for a index.js file whether you provide a path to a directory.

For example, require('src/components') will attempt to load src/components/index.js.

This behavior was later retained by modern bundlers like Webpack, although it is not part of the ECMAScript modules standard.

What are they used for?

Index files have 2 mains roles in recent front-end projects:

Act as proxy to make imports shorter

Given you have a Button.jsx component located in a components/Button folder among other related files, like styles.

If you want to use your component, you will have Button twice in the import path, as is:

import { Button } from 'components/Button/Button';

Now, add an index.js file in the Button folder with the following content:

export { Button } from './Button';

Thanks to the index file, you can make your component's import shorter:

import { Button } from 'components/Button';

Group child exports

In addition to have shorter imports, index files also allow to reduce their number.

Let's go back to our components folder but with 2 components in it: Button and Link.

If we want to use both in the same place without using any index file, here are the imports:

import { Button } from 'components/Button/Button';
import { Link } from 'components/Link/Link';

Now, add an index.js file in the components folder that exports both components:

export { Button } from './Button/Button';
export { Link } from './Link/Link';

We can now import our 2 components with only 1 line:

import { Button, Link } from 'components';

Isn't that beautiful? Not really, but we'll see that in the next section.

Why shouldn't they be used?

Unfortunately, using index files also has more or less annoying drawbacks.

Here is a probably incomplete list:

How index files prevent Webpack to split chunks

What is Webpack?

Webpack is a popular bundler for front-end applications, it takes your source files as input and generates bundles that can be executed by browsers.

To make your application faster, Webpack will automatically optimise it by removing unused code and by splitting the output code into smaller pieces called chunks when possible.

Webpack also allows lazy loading some code by using dynamic imports.

Here are some common use cases where these features could be helpful:

As you can see, the optimizations of Webpack are very interesting, and it is not desirable at all to break them, especially lately when mobile is taking an increasingly part of web traffic.

How Webpack split code

The point that will particularly interest us regarding Webpack Code Splitting is the fact that it will try to group the code by module to generate its chunks.

Any imported file is considered as a module by Webpack.

This means that when you create index files containing multiple exports, you are telling Webpack that all these exports are part of the same module, i.e. the index file.

Use case

To demonstrate the issue, I picked a pretty simple use case where I use 2 components in a page entrypoint:

Those components are respectively exporting a string containing several occurrences of their names to make them big enough to represent some standard components.

If you want to reproduce the following experiments yourself, here is the repository containing the demonstration code.

First case: usage of an index file

I will generate a bundle using Webpack 5 from a first entrypoint called withIndex.js where I use an index file to load both components:

// webpack.config.js

module.exports = {
  entry: {
    withIndex: './src/withIndex',
  },
};

// src/components/index.js

export { component1 } from './component1';
export { component2 } from './component2';

// src/withIndex.js

import { component1 } from './components';

document.body.innerText = component1;

window.doStuff = () =>
  import('./components').then(
    ({ component2 }) => (document.body.innerText += component2),
  );

As you can see, I kept a very basic Webpack configuration with the default optimisations.

If I run the webpack command, it will generate a unique withIndex.js bundle with the following (partial) content:

// dist/withIndex.js

(() => {
  'use strict';
  var o = {
    693: (n, o, e) => {
      e.r(o), e.d(o, { component1: () => t, component2: () => p });
      const t = '
component1
component1
component1
component1[...]',
        p = '
component2
component2
component2
component2[...]';
    },
  };
  (document.body.innerText = n.component1),
    (window.doStuff = () =>
      Promise.resolve()
        .then(t.bind(t, 693))
        .then(({ component2: n }) => (document.body.innerText += n)));
})();

You can see that both components have been embed in the generated bundle 😱

It means that your dynamic import will not lazy load the component2 as it should because of the index file.

Indeed, Webpack considered that both components come from the same index.js module (with the id 693 in the bundle), so he grouped them into a unique bundle!

Second case: no index file

I will now use a second entrypoint called withoutIndex.js where I import the components using their full paths:

// webpack.config.js

module.exports = {
  entry: {
    withoutIndex: './src/withoutIndex',
  },
};

// src/withoutIndex.js

import { component1 } from './components/component1';

document.body.innerText = component1;

window.doStuff = () =>
  import('./components/component2').then(
    ({ component2 }) => (document.body.innerText += component2),
  );

This time, Webpack will generate the 2 following bundles:

// dist/438.js

(self.webpackChunkindex_tests = self.webpackChunkindex_tests || []).push([
  [438],
  {
    438: (n, o, e) => {
      'use strict';
      e.r(o), e.d(o, { component2: () => t });
      const t = '
component2
component2
component2
component2[...]';
    },
  },
]);

// dist/withoutIndex.js

(() => {
  'use strict';
  (document.body.innerText =
    '
component1
component1
component1
component1[...]'),
    (window.doStuff = () =>
      c
        .e(438)
        .then(c.bind(c, 438))
        .then(({ component2: n }) => (document.body.innerText += n)));
})();

In this case, 2 bundles have been generated:

This time, Webpack split the code correctly and the component2 will really be lazy loaded 🎉

Conclusion

To conclude, even if the usage of index files has become widespread lately, it causes significant inconvenience which makes it undesirable in my opinion.

Sources

History