Source:  Twitter logo

I have a React project that uses Webpack as a bundler, and I'm splitting my bundle into two chunks -- the main codebase main.js, and the vendor bundle vendor.js.

After building these bundles, main.js ends up being 45kb and vendor.js is 651kb.

One specific vendor library is 225kb and seems to be the worst offendor in the vendor imports.

I am importing this library in a page component at the top of the file:

import React from 'react';
import { ModuleA, ModuleB } from 'heavyPackage'; // 225kb import

...

const Page = ({ setThing }) => {

...

};

To try and have this heavy import loaded in a separate bundle, I tried to instead import these modules using a dynamic import.

Inside the Page component, the modules weren't actually used until a particular function was called, so I tried to import the modules within that scope rather than at the top of the file:

import React from 'react';

...

const Page = ({ setThing }) => {

  ...

  const handleSignIn = async () => {
    const scopedPackage = await import('heavyPackage');
    const { moduleA, moduleB } = scopedPackage;

    // use moduleA & moduleB normally here
  };

};

For some reason I figured Webpack would intelligently pick up on what I'm trying to do here and separate this heavy package into its own chunk that is downloaded only when needed, but the resulting bundles were the same -- a main.js that was 45kb and a vendor.js that was 651kb. Is my line of thinking here correct and possibly my Webpack configuration is off, or am I thinking of dynamic imports in the wrong way?

edit I have Webpack configured to split the bundle using splitChunks. Here is how I have this configured:

  optimization: {
    chunkIds: "named",
    splitChunks: {
      cacheGroups: {
        commons: {
          chunks: "initial",
          maxInitialRequests: 5,
          minChunks: 2,
          minSize: 0,
        },
        vendor: {
          chunks: "initial",
          enforce: true,
          name: "vendor",
          priority: 10,
          test: /node_modules/,
        },
      },
    },
  },

Update for React 18: The code below is no longer required to split chunks/dynamically load components. Instead, you can use React.lazy with Suspense, which achieves similar results (this only works for React components, therefore any node_module imports would need to be imported within this dynamically loaded component):

const ProfilePage = React.lazy(() => import('./ProfilePage')); // Lazy-loaded

<Suspense fallback={<Spinner />}>
 <ProfilePage />
</Suspense>

@Ernesto's answer offers one way of code splitting by using react-loadable with the babel-dynamic-import plugin, however, if your Webpack version is v4+ (and has a custom Webpack config set to SplitChunks by all), then you'll only need to use magic comments and a custom React component.

From the docs:

By adding [magic] comments to the import, we can do things such as name our chunk or select different modes. For a full list of these magic comments see the code below followed by an explanation of what these comments do.

// Single target

import(
 /* webpackChunkName: "my-chunk-name" */
 /* webpackMode: "lazy" */
 'module'
);

// Multiple possible targets

import(
 /* webpackInclude: /\.json$/ */
 /* webpackExclude: /\.noimport\.json$/ */
 /* webpackChunkName: "my-chunk-name" */
 /* webpackMode: "lazy" */
 /* webpackPrefetch: true */
 /* webpackPreload: true */
 `./locale/${language}`
);

Therefore, you can create a reusable LazyLoad component like so:

import React, { Component } from "react";
import PropTypes from "prop-types";

class LazyLoad extends Component {
  state = {
    Component: null,
    err: "",
  };

  componentDidMount = () => this.importFile();

  componentWillUnmount = () => (this.cancelImport = true);

  cancelImport = false;

  importFile = async () => {
    try {
      const { default: file } = await import(
        /* webpackChunkName: "[request]" */
        /* webpackMode: "lazy" */
        `pages/${this.props.file}/index.js`
      );

      if (!this.cancelImport) this.setState({ Component: file });
    } catch (err) {
      if (!this.cancelImport) this.setState({ err: err.toString() });
      console.error(err.toString());
    }
  };

  render = () => {
    const { Component, err } = this.state;

    return Component ? (
      <Component {...this.props} />
    ) : err ? (
      <p style={{ color: "red" }}>{err}</p>
    ) : null;
  };
}

LazyLoad.propTypes = {
  file: PropTypes.string.isRequired,
};

export default file => props => <LazyLoad {...props} file={file} />;

Then in your routes, use LazyLoad and pass it the name of a file in your pages directory (eg pages/"Home"/index.js):

import React from "react";
import { Route, Switch } from "react-router-dom";
import LazyLoad from "../components/LazyLoad";

const Routes = () => (
  <Switch>
    <Route exact path="/" component={LazyLoad("Home")} />
    <Route component={LazyLoad("NotFound")} />
  </Switch>
);

export default Routes;

On that note, React.Lazy and React-Loadable are alternatives to having a custom Webpack config or Webpack versions that don't support dynamic imports.


A working demo can be found here. Follow installation instructions, then you can run yarn build to see routes being split by their name.

6 users liked answer #0dislike answer #06
Matt Carlotta profile pic
Matt Carlotta

Oki then, look! you have yow webpack config with the splitChunks property, also you need to add a chunkFilename property in side of the output object from webpack.

If we take for example the one generated by CRA

      // The build folder.
      path: isEnvProduction ? paths.appBuild : undefined,
      // Add /* filename */ comments to generated require()s in the output.
      pathinfo: isEnvDevelopment,
      // There will be one main bundle, and one file per asynchronous chunk.
      // In development, it does not produce real files.
      filename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].js'
        : isEnvDevelopment && 'static/js/bundle.js',
      // TODO: remove this when upgrading to webpack 5
      futureEmitAssets: true,

      // THIS IS THE ONE I TALK ABOUT
      chunkFilename: isEnvProduction
        ? 'static/js/[name].[contenthash:8].chunk.js'
        : isEnvDevelopment && 'static/js/[name].chunk.js',
      // webpack uses `publicPath` to determine where the app is being served from.
      // It requires a trailing slash, or the file assets will get an incorrect path.
      // We inferred the "public path" (such as / or /my-project) from homepage.
      publicPath: paths.publicUrlOrPath,
      // Point sourcemap entries to original disk location (format as URL on Windows)
      devtoolModuleFilenameTemplate: isEnvProduction
        ? info =>
            path
              .relative(paths.appSrc, info.absoluteResourcePath)
              .replace(/\\/g, '/')
        : isEnvDevelopment &&
          (info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
      // Prevents conflicts when multiple webpack runtimes (from different apps)
      // are used on the same page.
      jsonpFunction: `webpackJsonp${appPackageJson.name}`,
      // this defaults to 'window', but by setting it to 'this' then
      // module chunks which are built will work in web workers as well.
      globalObject: 'this',
    },

Once you have that on yow webpack. next thing is to install a npm i -D @babel/plugin-syntax-dynamic-import and add it to your babel.config.js

module.exports = api =>
...
return {
  presets: [
   .....
 ],
 plugins: [
....
"@babel/plugin-syntax-dynamic-import",
....
 ]
}

then last thing npm install react-loadable create a folder called: containers. in it place all the containers

inside index.js do some like:

The loadable object have two properties

export const List = Loadable({
    loader: () => import(/* webpackChunkName: "lists" */ "./list-constainer"),
    loading: Loading,
});
  • loader: component to dynamically import
  • loadinh: component to display until the dynamic component is loaded.

and for last on you Router set each loadable to a route.

...
import { Lists, List, User } from "../../containers";
...
export function App (): React.ReactElement {
    return (
        <Layout>
            <BrowserRouter>
                <SideNav>
                    <nav>SideNav</nav>
                </SideNav>
                <Main>
                    <Header>
                        <div>Header</div>
                        <div>son 2</div>
                    </Header>
                    <Switch>
                        <Route exact path={ROUTE_LISTS} component={Lists} />
                        <Route path={ROUTE_LISTS_ID_USERS} component={List} />
                        <Route path={ROUTE_LISTS_ID_USERS_ID} component={User} />
                        <Redirect from="*" to={ROUTE_LISTS} />
                    </Switch>
                </Main>
            </BrowserRouter>
        </Layout>
    );
}

so then when you bundle yow code we get some like:

2 users liked answer #1dislike answer #12
Ernesto profile pic
Ernesto

Copyright © 2022 QueryThreads

All content on Query Threads is licensed under the Creative Commons Attribution-ShareAlike 3.0 license (CC BY-SA 3.0).