javascriptwebpacklazy-loadingcode-splitting

Webpack puts lazy-loaded module into main chunk


I have two components.

One of them is imported directly:

import { directComp } from "./components";

another one is imported lazy:

const lazyComp = import("./lazyProxy");

lazyProxy.js:

import { lazyComp } from "./components";

export default lazyComp;

components/index.js:

export const lazyComp = () => {
  console.log("check lazyComp");
};
export const directComp = () => {
  console.log("check directComp");
};

When i bundle it, webpack puts both lazyComp and directComp into main chunk. Which is not what i expected. I wanted lazy-loaded component to be in a separate chunk.

If i change components/index.js:

export { default as directComp } from "./directComp";
export { default as lazyComp } from "./lazyComp";

Then webpack works as expected: directly imported directComp resides in main chunk. Lazy loaded lazyComp - in a separate chunk.

I would like to achieve the same result (directComp - in main, lazyComp - in a separate chunk) when all the code of these components is located inside one file, i.e.:

export const lazyComp = () => {
  console.log("check lazyComp");
};
export const directComp = () => {
  console.log("check directComp");
};

Here is a link to the sample repo: https://github.com/AlexanderSlesarenko/sample


Solution

  • Most bundlers don't seem to support your given usage pattern, i.e. they do not split modules across dynamic import() when combined with a normal import referencing the same module. In your case the common module being referenced is the index barrel file.

    See this comment on a webpack issue explaining lack of support. Additionally, you can find support, or lack thereof across other bundlers in Splitting Modules Between Dynamic Imports (seems browserify supports this).

    There are some things you can do if you want the main chunk to not include code from the dynamic imports that reference the same module, i.e. the specifier resolves to the same module. This requires changing how you import the code, however.

    Also note, to ensure proper tree-shaking you need to enable compress: true in the Terser plugin (currently you have it set to false). To see why tree-shaking doesn't work with dynamic import() without additional info (which I'll show), see this comment.

    Additionally you can add an extra export from the common (index) file to ensure tree shaking works:

    export const lazyComp = () => {
      console.log("check lazyComp");
    };
    export const directComp = () => {
      console.log("check directComp");
    };
    export const extraComp = () => {
      console.log('check extrComp');
    }
    

    Dynamically import all used exports from the common module

    This scenario doesn't use the lazyProxy and essentially uses one dynamic import to create a separate chunk from main, and removes the normal import.

    // Don't use this anymore
    //import { directComp } from "./components";
    
    // Other code that may be part of the main entry
    const main = 'true'
    
    // Load the entire common module dynamically
    import('./components').then(mod => {
      console.log(typeof mod.directComp === 'function') // true
      console.log(typeof mod.lazyComp === 'function') // true
      console.log(typeof mod.extraComp === 'function') // true
    })
    
    console.log('main', main)
    

    You can import this by using the webpackExports magic comment to only include the exports you want to use:

    import(/* webpackExports: ["directComp", "lazyComp"] */ './components').then(mod => {
      console.log(typeof mod.directComp === 'function') // true
      console.log(typeof mod.lazyComp === 'function') // true
      console.log(typeof mod.extraComp === 'function') // false
    })
    

    Bypass webpack code splitting and use native import()

    Requires:

    Add this to the plugins section of your webpack build:

    new CopyPlugin({
      patterns: [
        { from: "src/components/index.js", to: "components.js" },
        { from: "src/lazyProxy.js", to: "lazyProxy.js"}
      ],
    }),
    

    Change main.js to this:

    import { directComp } from "./components";
    
    import(/* webpackIgnore: true */ './lazyProxy.js').then(({ default: lazyComp }) => {
      console.log(lazyComp())
    })
    
    directComp();
    

    Change lazyProxy.js to this:

    // Just adds the .js extension
    import { lazyComp } from "./components.js";
    
    export default lazyComp;
    

    Note, technically you can bypass the lazyProxy.js and just do import('./components.js') which would require changing the copy plugin config.

    My opinion on barrel files

    Avoid them, particularly if you are authoring a library. They are not worth the hassle and create difficulty for proper tree-shaking and code splitting. Not to mention they tend to create cycles in projects utilizing them.