How we moved away from CRA to a bootstrapped bundling system. We recount the process and try to provide resources for developers in a similar situation.
Migrating away from CRA
Introduction
Whenever a new Single Page Application (SPA) has to be created using React, CRA is often suggested as the best way to begin. This is not just what random articles on the internet say, the React team themselves recommend this approach.
*The React team prominently suggested CRA as the de-facto solution for starting out writing SPA applications in React.
We took their advice and started our healthcare education SaaS project using CRA. Things were fine for the first few weeks of work. But as we approached implementing more and more complex parts of the project, CRA proved to be a hinderance. A few npm packages online that were crucial in our project required diving deep and changing the underlying webpack configuration. Internally changing the webpack configuration is not something CRA allows. We have to use configuration overriding packages to do so. Even then, we found ourselves frequenting online forums for solving issues which turned out to be specifically CRA (or config overriding) related. We had abstained from ejecting / migrating to a from-scratch setup, out of caution.
*This piece of CRA documentation aptly describes some attributes of a from-scratch setup.
Some of our concerns were:
  • What would be the cost of maintaining the webpack (and webpack-related) system?
  • How much time would we have to invest in fixing all the breaking changes?
  • What new problems will the migration introduce?
  • Will they be significant enough to hinder our delivery timeline?
We decided to stick with CRA due to these concerns and more until...
The Plunge
We had an adjacent project at hand. This was the migration of our website from its old form to a sharper and modern look. For this project, we decided to incorporate learnings from our previous project, and take the plunge of diving into a from-scratch setup. We decided to dive deep into webpack as our bundler of choice.
As we spent time reading about and exploring the world of webpack, it became clear how powerful it was. Using webpack, we had access to various configuration options for our bundling process. We additionally realized that it was not "just a bundler". Its extensive plugin system meant that we had a ton of control over various facets of the developer experience in addition to build generation.
Because we had the privilege of starting our project from scratch, we decided to go with the latest version of webpack at the time (webpack 5). With webpack in general, we are expected to write a ton of boilerplate for the configuration file. There‘s a lot of working parts which only make sense if one has been through the process. Thankfully, this only needs to be done once. We followed a very extensive video guide by YouTuber Swashbuckling with Code that took us 90% of the way through.
If there were issues that were encountered in the process, the discourse in the developer community seemed to be very sparse, especially due to us using the latest webpack version at the time. Nevertheless, between the video, official documentation, GitHub threads, and Stack Overflow, we had enough assistance to reach a fairly suitable configuration for our needs. We'll highlight some areas with regards to the switch.
Power of Aliases
Aliasing is when, instead of using relative imports, we can use specific directories as our import path base. This looks something like this:
Loading code files...
Unfortunately, doing this means that all the relative imports of `ComponentA.jsx` need to be updated. We use VSCode as our editor of choice, and while it does offer auto-update of import statements on reindexing, the process is not as seamless as we'd hoped. It often takes a few seconds to kick in, and sometimes we might want to make other related changes before properly wanting the import statements to update. But more importantly, auto-updating relative imports always felt like a bandage solution to a problem which shouldn't exist in the first place. This is where import aliases became a life saviour for us. With aliasing, we could move around and rename components as much as we wanted. There was no need to change any import statements within. This greatly reduced friction in our development workflow.
Ease of Optimizing Assets
Webpack has a myriad of plugins to optimize assets. They have extensive first party documentation for the same. Aside from caching optimization that we specifically applied to our `javascript` and `css` assets, most of our efforts were invested in optimizing our images. For this too, we found a mature third party plugin for our purposes, ImageMinimizer. Documentation surrounding this plugin is very extensive, with many examples being provided for a variety of use-cases. With some tinkering, we found that lossy optimization worked well for us. But what took it to the next step was our migration to `webp` assets. Here too, webpack's "generator" system resulted in a very simple way of converting existing imported assets into `webp` during build time. (A simple import change from `import image from "assets/image.png";` to `import image from "assets/image.png?as=webp;"`).
If the developer decides to use webp as their format of choice, specifically using generators (i.e. the webp files are generated in the build step), the de-facto method specified in the documentation here might not work:
Loading code files...
It must be remembered that generating `webp` happens at the build step. As such, these changes won't be visible in the development environment.
Bundle Analysis
Considering how many layers already exist in a typical web app, it's crucial that we try to stay as vigilant as possible when it comes to the final bundle that our clients have to download. Enter, Webpack Bundle Analyzer.
Bundle Analysis
This plugin helps us visualize the relative sizes of our bundle components. As such, anything taking a disproportionate amount of space stands out. It helped us in optimizing a crucial (but heavy) dependency. We frequently check in on this analysis just to make sure everything is relatively light.
Miscellaneous
There were many other migration related points. We'll briefly mention them here.
Tailwind:
In our projects, we use `tailwind` for our styling purposes. One of our concerns before switching was incorporating the library in our projects. The official `tailwind` documentation only covers installation for CRA applications. But this proved to be non-issue. Many online video guides exist for this. Even though a majority of them are outdated (at the time of this writing), we still managed to combine learnings from a few up-to-date guides to help us out.
Fast Refresh:
Fast Refresh is React's take on Hot Reloading. Whereas normal Hot Reloading involves loss of local state in the development environment on code change, Fast Refresh attempts to update pages while retaining said state. And with the way this feature has been implemented, page updates are blazing fast. This has existed in an "experimental" state for some time now, but since CRA uses it internally, it seems to be stable enough to be used in actual development. (We implemented this in our code using the above mentioned video).
Source Maps:
Coding would indeed be very difficult if we couldn't source our issues to their code origins. Thankfully, webpack has an incredible selection of source maps to choose from. There's also information on which source map would be preferable for production as opposed to development with additional comments to qualify them.
Conclusion
Let's go back to the healthcare education SaaS project we mentioned earlier. After our learnings on `webpack` had reached a certain level of maturity, we felt confident enough to migrate our healthcare project away from CRA. While not painless, the migration was unusually simple. It all happened in the span of basically one day. A great thing about the plugin architecture of `webpack` is the fact that we could choose to introduce features one by one, making sure everything was working each step of the way.
Our source code remained largely untouched in our migration process. (Only the entry points needed some changes, (`src/index.js`), and that too because we had migrated to a newer version of React, and not due to using webpack per-se). Most of the migration work was isolated to all the meta-files surrounding the source code. This resulted in an up-to-date bundling and development system with a ton of transparency. This migration enabled us to utilize previously out-of-reach packages as well.
In conclusion, I would recommend at least trying to understand `webpack`, even if it doesn't result in the migration of your project. At the very least, "bundling" will no longer be a magical process.
Thinkgrid Logo
Transforming Ideas into Innovative Digital Solutions
Contact Us
Company
Social
Resources
Get in touch with us
© 2026 Thinkgrid Labs. All rights reserved.