Analyzing and Reducing React Bundle Size
Bundle size matters.
Hi everyone!
So, last day I read a story about how Netflix reduced their landing page’s bundle size and it’s inspired me, they removed React and used vanillaJS instead, but I can’t do that. I have limited time for this particular job so I am going to aim biggest reduce in shortest period of time.
Of course we use compression on server side to make sure our users always get smallest javascript file but even after compression, the size is considerably big for a landing page.(and of course a great power like compression comes with great responsibility - decompression-)
Before we get started let me to note that; I ejected my create-react-app and if you haven’t you should do it.
npm run eject
Note: Once you eject you can undo it. Be careful!
this will start the process of ejecting from Create React Native App's build scripts. You'll be asked a couple of questions about how you'd like to build your project. Once this command has successfully run, you should also follow any steps below that are applicable to your environment.
First Step: Analyze the bundle
we should install a npm package called “source-map-explorer”.
npm install --save source-map-explorer
you may use yarn as well.
yarn add source-map-explorer
Then in package.json
, add the following line to scripts
:
"analyze": "source-map-explorer build/static/js/main.*",
let’s roll.
npm run build npm run analyze
this will open a new tab in your default browser, let’s take a look at it
so this’s how a bundle analyze look like ? pretty complicated right? Not really.
It turned out the problem was our laziness. Our landing page had been a part of our main application before, but months ago we separated them. We forgot to remove a bunch of unnecessary packages and they made the project way more bigger than it should be.
Let ‘s take a closer look, and you can easily see our biggest players are moment, react-bootstrap, sugar, react-dom(react)and redux-form, jquery(somehow).
Moment problem
Moment is really awesome package, easy to use and pretty powerful. It’s been my favorite for handing date easily since the very begging of my web development carrier, but it’s really big.
It’s core only 52KB but for some reason it imports all locale files at once. It’s not modularized so you carry around all the world languages’ locale files in your app.
I don’t want that, I only have used tr, so let’s get rid off useless locales, unfortunately for us, neither its documentation nor its core includes any solution fits for us, but we can always use Webpack for that job. We gonna use webpack.ContextReplacementPlugin.
You might be using some big packages like Moment, always try to use modularized packages so you don’t have to import all library at once.
add the line below to your config/webpack.config.prod.js (you can add this to webpack.config.dev.js as well, but it might or might not slow down dev build progress and may cause latency in live review.)
// find plugins part and add this line,
plugins: [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
you can change /en/ part with whatever locale you want. let’s rebuild our app and analyze it.
as you can see, moment’s size reduced to 59kb with just one line.
Sugar is harmfull!!
Just kidding(Unless you are on some kind of diet, or have diabet disease), We used Sugar for some reason(don’t remember why, could not figure out neither).
When I looked up it’s documentation, I realized we’ve been doing it wrong. Our import statements like this;
const Sugar = require('sugar');// or import Sugar from 'sugar'
but it’s super wrong. Sugar is modularized and you should import the part you want to use.
import Sugar from 'sugar/number'// or const Sugar = require('sugar/number')// you don't have to change any code,
now this should reduce your bundle size, but it wasn’t enough for me and like I said before I don’t even remember even why this package used for, I guess it is one of the remainders of our main project . so I changed Sugar with Vanilla JS and get rid of it all.
Redux-form? Wait, Where did I use that?
Okay, so I use Redux-form in most of my project but I don’t remember using it in landing page so I searched it in project and find the leak. Damn! How did I forget that?
I just combined it with other reducers and forget.
import { combineReducers } from 'redux'
import { reducer as form } from 'redux-form'
import authReducer from './reducer_auth'
import fetchReducer from './reducer_fetch'
import {reducer as toastr} from 'react-redux-toastr'
import pageReducer from './reducer_page';
import { routerReducer as routing } from "react-router-redux"
const rootReducer = combineReducers({
form,
routing,
toastr,
auth: authReducer,
data : fetchReducer,
//post : postReducer,
// validate : validateReducer,
page : pageReducer,
});
export default rootReducer;
I removed it and our bundle size reduced to 877kb, good but not enough.
By the way, redux’s itself is also is a remainder. I’ll remove it completely
Next step replacing React with Preact
Preact is a lightweight alternative of React. It’s core only 4kb and it mostly compatible with React.
Disclaimer: Even though Preact claims it’s compatible with React and its ecosystem, there are still some packages(such as react-router) do not work as expected and may cause your application crash.
yarn add preact preact-compat
Preact package includes core and dom management and preact-compat is required for React compatibility.
Now we have successfully downloaded preact packages we can proceed to replacing react with preact.
In order to do that you must have ejected your app.
go to config/webpack.config.dev.js and config/webpack.config.prod.js and search for alias and add the following lines into it.
alias: {
"react": "preact-compat",
"react-dom": "preact-compat" // there might be a defination for react native, keep it.}
After this restart your app and check does preact work compatible with your packages.
Unfortunately it did not for me and react-router caused errors.
Next step: replacing react-router with preact-router
It’s easy to do but if you already have defined too many routes don’t try to do that and remove Preact.
yarn add preact-router
you can find documentation over here.
After all this steps my bundle size reduced to 577KB, good enough for now.
these steps worked out for me well, I didn’t lost any functionality but I can not give you any guarantee that it will work out in your project as well.
Alternative : Code Splitting
what if your application does not include any unused libraries ? or your application too big to even trying to replace Preact with React ? what can you do then? Well you’d rather use code splitting, fortunately for us, create-react-app comes with the support of code splitting.
Route based splitting might be the best choice to code splitting. Check out React documentation;
How to add route-base code splitting to your app?
first import Suspense, lazy from React.
import React, {Suspense, lazy} from 'react';
now replace your import lines with React lazy loading
// replace that line
import Index from './components/Index'
// to this
const Index = lazy(() => import('./components/Index'))
and now add Suspense and you are ready to go.
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Index}/>
</Switch>
</Suspense>