Screaming Architecture and Reducing bundle size of react-apps

Subham Saha
TechVerito
Published in
5 min readApr 1, 2024
Photo by Walkator on Unsplash

Disclaimer :-

The following is an experience I had working with a live project, so some names of libraries and components have been redacted.

I was tasked to come up with a strategy to integrate a new frontend application as part of an existing web application. As the existing application was already quite bulky and huge I presented the idea of having a micro-frontend approach. This would provide us the ease to use the libraries and framework of our desire, and even structure it according to the team’s desire and decision.

A Tryst with Screaming Architecture

Some time back while looking for options I came across the idea of Screaming Architecture by Uncle Bob which is slightly different than a regular react application, where every functionality is broken into various components and placed under the components directory under src. Whereas we decided to break every functionality as a feature and export a single/couple of component(s) from a particular feature which needed to interact with other features. This would help us keep things abstracted and segregated. So every feature could have the following structure :-

src/features/awesome-feature
|
+ - api # exported API request declarations mostly graphql mutations and queries
|
+ - components # components scoped to a specific feature
|
+ - hooks # hooks scoped to a specific feature
|
+ - routes # route components for a specific feature pages
|
+ - stores # state stores for a specific feature
|
+ - types # typescript types for TS specific feature domain
|
+ - utils # utility functions for a specific feature
|
+ - index.ts # entry point for the feature, it should serve as the public API of the given feature and exports everything that should be used outside the feature

I also came across this repository which served as a very good starting to point to see the concept implemented.

Trouble brewing in the distant horizon :-

So in the early days of development everything was smooth and quick. But as the functionality of our application started growing, and we kept on adding features we started noticing the initial load time and bundle size of our application to also go up. This was a point of on-going discussion within the team and also other stakeholders reported this issue to us couple of times. We knew even though this isn’t a major issue now but it would soon turn out to be a BIG problem when we keep adding features.

Team ASSEMBLE! :-

So after completing our initial development we decided to take a look at this issue and try and address it.

So the very first step we tried was visiting react documentation to understand what they have to suggest about this issue. We came across couple of suggestions :-

  • Use source-map-explorer and execute the following command to understand what was causing the issue of increased bundle size “analyze”: “source-map-explorer ‘build/static/js/*.js’”
    This revealed to us that our app was being bundled as a single js file. We needed to split the same.
  • But as we had initiated our app out of CRA, and haven’t performed
    npm eject the webpack configurations were hidden from us. To provide our own configurations we were using craco which acts as a wrapper over webpack.
  • Next up we used the two code splitting techniques suggested by the react documentation,

i. splitting by routes :- initially we had two routes defined for our app.
/courses to route to the Courses List page, and /courses/{courseId} to route to the course details page.
But now we introduced two other routes, /course/create-course to route to the Create Course Form page and /course/edit-course/:courseId to route to the same Create course form page but when editing a course.

ii. splitting by dynamic import :- we also introduced lazy loading of components mainly modals, popovers which wasn’t required for the very first time of the page load.

Actions bearing fruit :-
After performing the two code splitting techniques we ran the BundleAnalyzerPlugin that gave us a report, stating adding the lazy loading did introduce smaller bundles of those components. Refer to the two images below,

Image 1, pre lazy loading, only having one js in the bundle
Image 2, post lazy loading, having smaller bundles of the lazy loaded components
  • Next up we noticed lodash and component library(icons and core) taking up most of the bundle size as easily noticeable from the above image, this happens when we import the complete library in our imports even when using a single function/component from the library.
  • for eg — import { isEmpty } from ‘lodash’ or
    import { Heading } from @compnent-library
  • To prevent doing this we can import specific libraries from lodash like
    import isEmpty from ‘lodash/isEmpty’.
    This syntax imports the isEmpty function directly from the isEmpty module within the lodash library. This approach is called "named export" and it directly imports the specific function from the specified path.
  • this helped us understand we can also do something similar for the components library imports.

After doing the following changes we saw further improvement in the bundling of our app, refer to the below image.

Image 3, core library bundle has been squeezed up after individual import of components.

To enforce this is being followed by future developers we can add a eslint config as

{
"rules": {
"custom/no-named-imports-from-lodash": [
"error",
{
"module": "lodash",
"allowedNamedImports": ["isEmpty", /* Add other allowed named imports here if needed */]
}
]
}
}

So doing these small steps we were able to affect our bundle size and we were able to notice a significant betterment in the initial load time of our application.

--

--