create-react-app reduce build size: main.[hash].chunk.js is 3MB+ with mostly images

This is a tough problem that most engineers have as the app grows larger.

In your case you're importing a lot of small PNGs that get base64 encoded due to CRA's webpack setting. You can override that behavior with a library like react-rewired. While it's a bit more involving process, I'd recommend that over maintaining an external image repository. It's easier to test and use in a development environment. Especially when offline.

I also see that most of your views are bundled together in main.js.

The secret to cutting down the bundle size is in leveraging code splitting and lazy loading.

Here's the link to the video that I put together for this: https://www.youtube.com/watch?v=j8NJc60H294

In a nutshell, I suggest the following patterns:

Code-split routes (if any)

This is commonly achieved by using React.lazy()

import React, { Suspense, lazy } from 'react';
// import MyRoute from 'routes/MyRoute' - we are replacing this import

const MyRoute = lazy(() => import('routes/MyRoute'));

const Loading = () => <div>Loading route...</div>;

const View = () => (
  <Suspense fallback={Loading}>
    <MyRoute />
  </Suspense>
);

Code-split low priority elements

Every page has Critical Path content. It's the content your visitors want to experience first. It's the primary purpose of a view. It's usually in the above the fold area (the part of the view they see when the page loads, without any scrolling).

However, that part can also be dissected based on priority. A Hero is a good critical path example, so we should prioritize it. A Hamburger menu is a great element to de-prioritize because it requires interaction to be viewed.

Anything below the fold can be de-prioritized too. A great example of this is Youtube comments. They only get loaded if we scroll down sufficiently.

You can follow the same principle as above to prioritize critical path content:

import React, { Suspense, lazy } from 'react';
import Hero from 'component/Hero'

const Burger = lazy(() => import('components/Burger'));

// The fallback should be used to show content placeholders.
// It doesn't have to be a loading indicator.
const Loading = () => <img src="path/to/burger/icon" alt="Menu"/>;

const View = () => (
  <main>
    <Hero />
    <Suspense fallback={Loading}>
       <Burger />
    </Suspense>
  </main>
);

Create abstract components for libraries

Following the principles above, I suggest creating simple abstract components for every library. Let's say you need animations and you use Framer Motion. You can import it in one place and use everywhere.

Many libraries ship with named exports and lazy() doesn't support that. I wrote a very simple library react-lazy-named that helps with that.

// AnimatedDiv.js

import React, { lazy } from 'react';

// This is similar to
//   import { motion } from 'framer-motion';
//   const Div = motion.div;
// Also, we hint webpack to use resource preloading (think service worker goodness)
const Div = lazy(() => import('framer-motion' /* webpackPreload: true */), 'motion.div'));

// Use regular div as a fallback.
// It will be replaced with framer-motion animated Div when lazy-loaded
const AnimatedDiv = (props) => (
  <Suspense fallback={<div>{props.children}</div>}>
    <Div {...props} />
  </Suspense>
);

If you have any questions or need and help with this you can reply here or in the comments of the aforementioned video on Loading React apps in under 3 seconds


Basically, anything you import in your app and handle bundling it with webpack would be considered as a dependency.

so: import img from path/to/img would make it a dependency, thus, included in your bundle, that's what you want to escape.

There are two possible scenarios to work-around this:

  1. Stop importing images, make them available/hosted in a CDN such like AWS S3.
  2. Apply a Move Statics Out approach, which is about moving all directories for static files out of the bundle, make them available in statics dependent folder and start using relative links instead, I would suggest copy-webpack-plugin.

For me, I would go with number 1.


Small image files will be loaded into your code when build with url-loader to reduce number of image requests. Large image files will be copied to build folder with file-loader.