We are using create-react-app
and Typescript within all of our projects and a slightly large module of commonly used React components has emerged. Trying to factor these out into its own NPM package (for easier maintenance and better reuse), here called PackageA
, we have arrived at a situation where the overall size of a test app, here called TestApp
, is larger than it was before (when the same code existed inside the code base of the same app). TestApp
is a VERY rudimentary app which basically just showcases some (but not all) parts of the components in PackageA
, formerly with the components inside the project itself, and now with this part removed and instead imported from privately published PackageA
.
Sizes of initial JS chunks before factoring out PackageA
with components from TestApp
("main" is assumed to be code from the project itself and the other chunk is believed to hold dependencies):
Sizes after factoring out PackageA
from TestApp
:
As can be seen, the overall size increases with 13 kB gzipped and 56 kB unzipped. This corresponds to an increase of ~ 6% gzipped and ~ 8% unzipped. This is not insanely much but I would still expect them to be somewhat similar.
PackageA
is published as es6 modules to allow for tree-shaking which seem to work properly since unused parts of PackageA
are not emitted into the output chunks of TestApp
.PackageA
is done by UglifyJS prior to publishing using both --compress
and --mangle
options.PackageA
but only available separately.dependencies
in PackageA
's package.json
are NOT used anywhere else in TestApp
and were subsequently removed from the TestApp
's package.json
at the same time as they were added to PackageA
's package.json
. The same versions were used. All other dependencies of PackageA
are listed under peerDependencies
.node_modules
folder and running a fresh install of dependencies.PackageA
is exactly the same as the deleted folder from TestApp
with the exception of an added index.ts
page which exports the contents of the other files. This file is < 3 kB unzipped in size.What could be possible sources of this increase in size and where could we start looking? Perhaps this increase might lie in how the code is transpiled in the package and that create-react-app
is somehow able to do this more efficiently for code included in the project itself than for imported code. I know this is a tough question that might have many answers and that is hard to reply to.
This is the tsconfig.json
used in the factored out PackageA
:
{
"compilerOptions": {
"target": "es6",
"lib": ["es6", "dom"],
"jsx": "react-jsx",
"module": "es6",
"rootDir": "./src",
"moduleResolution": "node",
"declaration": true,
"sourceMap": true,
"outDir": "./build/esm",
"inlineSources": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
},
"include": ["./src"],
"exclude": ["**/*.test.tsx", "**/*.test.ts", "**/*.stories.tsx"]
}
I understand that this questions might be highly dependant on the situation you're in but still thought I'd share my process.
Usually when you build, your framework outputs the sizes of the built artifacts. In the case of create-react-app
, these sizes seem to be the size of the gzipped versions.
You may also check your browser dev tools and note the sizes of the downloaded bundles which are typically NOT the gzipped sizes but the actual sizes.
When factoring something out or doing some sort of comparison, take notice of the different sizes before the change and after the change. Given some sort of intuition as to how these sizes should change evaluate if this is the case or not.
With NextJS you may use @next/bundle-analyzer
, in most other cases you may use source-map-explorer
. The latter uses source maps to map bundled code to sources; if the source itself has source maps, then these sources are used, otherwise you will see references to node_modules
. With source-map-explorer
, it might be worth both looking into the visual representation as well as the json representation since, with a lot of sources, the visual representation might suppress the participation of a certain component or library which the json representation will not.
With source-map-explorer
you may make some valuable comparisons between different versions of an app.
After establishing with, for example, source-map-explorer
that some source code expands more in some context that in another, you may want to research the issue further. Using source maps (either in development mode or in production mode if you have them available there), you may want to track the imprint of a certain portion of source code in the output bundle. A lot of browsers support mapping bundled code to source code (given source maps) but not all allow you to do the opposite. Firefox, however, has this feature and it allows you to, given bundled code, view the source code and vice versa. This is very useful when you're trying to track what some source code actually results in in the output.
Well, the following is a list of what caused the increased size in my case:
I imported from an index in my package and the index imported all of the exports of the package. This should not be a problem with esm modules, but without the sideEffects: false
flag in package.json
there were some ambiguities. Even though the unused sources from MY package were not added to the output bundle, some indirect dependencies could not be ruled out as not having side effects, so these were added to the bundle, even though the component that imported them were not even used. Adding sideEffects: false
to the package.json
of my package solved this and reduced the package size.
When keeping the components in my main project that was bundled using create-react-app
, some convenience methods added by Babel in all transpiled files were removed thanks to the presence of @babel/plugin-transform-runtime
plugin. This plugin removes these convenience methods, given that some Babel runtime was guaranteed to be present when running these files (from which they could be imported instead). When the same configuration was used for the factored out components, the size shrank even more.
Finally, at the time of this writing, the tsc
compiler seem to output more verbose code than Babel (see TypeScript: tsc transpiles jsx to more verbose code than babel). When transpiling the package I first used tsc
and after started using Babel, the size shrank even more.
All in all, with the above changes, the output ended up being just 1.7 kB larger (unzipped) than before the refactoring which was a both understandable and acceptable result.