Hi, I'm Sagar working as a senior software engineer. I love to write articles which will help developers to understand the magic of JavaScript. If you have any questions about the article, leave a comment and I'll get back to you, or find me on twitter @sagar_dev44.
In the JavaScript eco-system, there are excellent libraries and framework available on the package manager registry and in day to day life we are importing it into our project. That's okay when you have just started your project but as soon as your project grows your facing a lot of performance related issues.
In this article, we are going to focusing on common issues like a large bundle size slow startup and resolving it simply implement code splitting in React app.
Bundling
Mostly modern apps are "bundled" down to single file by using Webpack or Browserify. Bundling codebase is the excellent way until your app is pretty small with limited dependencies. As soon as your codebase grows, your bundle size will grow too and then problems happening started like a large bundle size, slow startup, and slow hot module replacement etc.
If your curious about how bundling works, I strongly recommanded you to read official documentation of webpack.
Code Splitting
The perfect solution for handling a large bundle size and slow startup is to implement code splitting in you're app i.e. split your code into smaller chunks which can then be loaded on demand or in parallel.
Best practice is to keep your chunks size under 150KB, so that the app becomes more interactive within 3–5 seconds, even on poor networks.
The significant benefit of creating apps with Create React App, Next.js, or Gatsby, because they provide code splitting setup out of the box or you can set up by yourself.
If you want to setup code splitting by yourself see the Installation and Getting Started guides on the Webpack docs.
import()
– dynamically import ES modules
The best way to start introducing code splitting into your app is through the dynamic import(). It enables us to dynamic loading of ES modules. By default, ES modules are completely static. You must specify what you import and export at compile-time and you can’t change it at runtime.
import CONSTANTS from './constants/someFile.js'; // importing CONSTANTS from someFile.js by using es import
ES modules have few limitations like es module should only appear at the top level of a file means if we mention any statement above es module import it will throw an error and another is that a module path is fixed we can't compute or dynamically alter it.
For example,
const double = (x) => x*x;
import CONSTANTS from './constants/someFile.js'; // it will throw an error because we created double function above es import module
On the other side, both es module limitations have overcome by dynamic import() es module and also provides asynchronous module importing feature.
const modulePath = './someFile.js'; // path of module
// dynamic import() module
import(modulePath).then(module => {
return module.default; // return default function of es module
});
With dynamic import()
we can specify the es module path or we can alter path in runtime and it returns a promise and we have to handle this promise in .then()
method or .catch()
method if it throws an error.
Note that, the dynamic import()
syntax is an ECMAScript (JavaScript) proposal not currently part of the language standard. It is expected to be accepted in the near future.
There is two way of implementing code splitting in your app as route-based
or component-based
code splitting. You have to decide where in your app to introduce code splitting can be a bit tricky.
Route based code splitting
A good place to start code splitting is with app routes. Break down an application into chunks per route, and then load that chunk when user navigate that route. Under the hood, webpack takes care of creating chunks and serve chunks to the user on demand.
We have to just create asyncComponent and import the desired component by using dynamic import()
function.
Let's create an asyncComponent
component which takes the desired component through dynamic import()
return a promise for a component. After component promise has been successfully resolved then it returns the desired component. In simple word, dynamic import()
imports component asynchronously.
// filename: asyncComponent.jsx
import React, { Component } from "react";
const asyncComponent = (getComponent) => {
// return AsyncComponent class component
return class AsyncComponent extends Component {
static Component = null;
state = {
Component: AsyncComponent.Component // first time similar to static Component = null
};
componentWillMount() {
if (!this.state.Component) {
// if this.state.Component is true value then getComponent promise resolve with .then() method
// For simplicity, I haven't caught an error, but you can catch any errors or show loading bar or animation to user etc.
getComponent().then(({ default: Component }) => {
AsyncComponent.Component = Component;
this.setState({ Component }); // update this.state.Component
});
}
}
render() {
const { Component } = this.state; // destructing Component from this.state
if (Component) {
// if Component is truthy value then return Component with props
return <Component {...this.props} />;
}
return null;
}
};
};
export default asyncComponent;
We are doing a few things here:
- The
asyncComponent
function takesgetComponent
as an argument that when called will dynamicallyimport()
function a given component. - On
componentWillMount
, we simply resolve promise with.then()
method and then mutatethis.state.Component
state to the dynamically loaded component. - Finally, In
render()
method we are returning loaded component fromthis.state.Component
withprops
.
Now, it's time to use asyncComponent
. Start with separating routes of an app with react-router-app.
// filename: index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import asyncComponent from "./asyncComponent";
// import components with asyncComponent (indirectly using dynamic import() function)
const App = asyncComponent(() => import("./App"));
const About = asyncComponent(() => import("./About"));
const PageNotFound = asyncComponent(() => import("./PageNotFound"));
ReactDOM.render(
<Router>
<Switch>
<Route path="/" component={App} exact />
<Route path="/about" component={About} exact />
<Route component={PageNotFound} />
</Switch>
</Router>,
document.getElementById("root")
);
If you run yarn run build
with an app created by Create React App
, you’ll see our app has been split into several chunks.
# Before implementing code splitting
File sizes after gzip:
38.35 KB build/static/js/1.3122c931.chunk.js
797 B build/static/js/main.70854436.chunk.js
763 B build/static/js/runtime~main.229c360f.js
511 B build/static/css/main.a5142c58.chunk.css
# After implementing code splitting
File sizes after gzip:
38.33 KB build/static/js/5.51b1e576.chunk.js
1.42 KB build/static/js/runtime~main.572d9e91.js
799 B build/static/js/main.3dd161f3.chunk.js
518 B build/static/js/1.5f724402.chunk.js
327 B build/static/css/1.f90c729a.chunk.css
275 B build/static/css/main.6a5df30c.chunk.css
224 B build/static/js/2.4a4c0b1e.chunk.js
224 B build/static/js/3.76306a45.chunk.js
If you clearly observed chunks size, except two or three chunks remaining all chunks size are below than 100KB.
Don't overthink about asyncComponent
coding stuff later we will introduce a React-Loadable
library which gives us a flexible apis for achieving code splitting.
Component-based code splitting
As we saw earlier, route based code splitting is pretty straightforward where we break down chunks as app route.
If your specific route is too complex where the massive use of UI components, models, tabs etc. and has chunk size goes bigger then standard chunk size like 150KB. In such a scenario we have to move one step forward for splitting code on basis of components also called as component based code splitting.
// filename: App.jsx
import React, { Component } from "react";
import asyncComponent from "./asyncComponent"; // imported asyncComponent
// simple class based App component
class App extends Component {
constructor(props) {
super(props);
this.state = {
Greeting: null // <== initially set to null
};
}
// handle button clicks
handleButtonClick = () => {
if (!this.state.Greeting) {
// load Greeting component with dynamic import
const Greeting = asyncComponent(() => import("./Greeting"));
this.setState(prevState => {
return {
Greeting
};
});
}
};
render() {
const { Greeting } = this.state; // grab Greeting component from state
return (
<React.Fragment>
<button onClick={this.handleButtonClick}>Click me</button>
{Greeting && <Greeting message="lorem ipsum dummy message" />}
</React.Fragment>
);
}
}
export default App;
We are doing a few things here:
- We have created a simple
<App />
class component with abutton
. - In
<App />
component, on button click we are dynamically importing<Greeting/>
component and stored insidethis.state.Greeting
state. - In render() method, firstly we destructuring
Greeting
fromthis.state
and stored in aGreeting
constant. Later with logical&&
(AND) operator, we cross-checking that it's notnull
value. Once Greeting is truth value then we are utilizing<Greeting />
component directly intojsx
. - Behind the scene, Webpack create separate chunk for
<Greeting />
component and serve to the user on demand.
React Loadable
React Loadable
is a small library designed by @jamiebuilds, that makes extremely easy to implement code splitting in React apps. It accomplishes code splitting by using dynamic import()
and Webpack.
React Loadable
provides Loadable
higher order component which lets you dynamically load any module before rendering it into your app.
Install react-loadable package into your app by using npm or yarn.
yarn add react-loadable # I'm sticking with yarn for this article.
Implement router based code splitting using React Loadable
React Loadable
is pretty straightforward, you haven't required to make any asynchronous component or not required to write complicated setup. Just import Loadable
component and provide loader
.
// filename: index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Loadable from 'react-loadable';
const Loading = () => <h1>Loading...</h1>; // loading component
// dynamic loading <App />, <About /> and <PageNotFound /> components
// Loadable is higher order components. it takes loader which dynamic import() of desired component
// and loading which component shows during successfully resolving dyanmic import()
const App = Loadable({
loader: () => import("./App"),
loading: Loading
});
const About = Loadable({
loader: () => import("./About"),
loading: Loading
});
const PageNotFound = Loadable({
loader: () => import("./PageNotFound"),
loading: Loading
});
ReactDOM.render(
<Router>
<Switch>
<Route path="/" component={App} exact />
<Route path="/about" component={About} exact />
<Route component={PageNotFound} />
</Switch>
</Router>,
document.getElementById("root")
);
Implement component based code splitting using React Loadable
Component based code splitting is as simple as we have already seen in the previous section.
import React, { Component } from "react";
import Loadable from "react-loadable";
const Loading = () => <h1>Loading...</h1>; // loading component
class App extends Component {
constructor(props) {
super(props);
this.state = {
Greeting: null
};
}
handleButtonClick = () => {
if (!this.state.Greeting) {
// load Greeting component with Loadable component
const Greeting = Loadable({
loader: () => import("./Greeting"),
loading: Loading
});
this.setState(prevState => {
return {
Greeting
};
});
}
};
render() {
const { Greeting } = this.state; // grab Greeting component from state
return (
<React.Fragment>
<button onClick={this.handleButtonClick}>Click me</button>
{Greeting && <Greeting message="lorem ipsum dummy message" />}
</React.Fragment>
);
}
}
export default App;
I hope you have enjoyed this article. If your curious or want to explore more in code splitting I have provided great references for you.
You have done code splitting in React. Now, It's party time.
References:
- https://reactjs.org/docs/code-splitting.html
- https://developers.google.com/web/fundamentals/performance/optimizing-javascript/code-splitting/
- https://hackernoon.com/effective-code-splitting-in-react-a-practical-guide-2195359d5d49
- https://alligator.io/react/react-loadable/
- https://webpack.js.org/guides/code-splitting/
Top comments (8)
enough talk.. now let's face real problem (: ...
I am making an instagram clone and I am facing this problem
"How to make react router replace component only after the matched route asynchronously loaded..?"
I created a post here explaining the problem dev.to/iamruhul/how-to-make-react-...
Anyone know how to solve this problem??
@sagar , nice article. Also notice there is some libraries out there focused in to improve user interaction by taking advantage of code splitting, and lazy loading based on user visibility, example:
Thanks @yosbel for links.
t completely agreed with your opinion. Thanks for feedback.
Awesome article! Just want to note that the import() syntax is not a function. But that's nit-picking... :) Awesomely done and compiled!
Hi Jannis, you're right it's not like traditional function, but simplicity, I introduce import() as a functional form for better understanding.
Thanks for your valuable feedback.
great article.
really helpful for me.
you are great programmer bro.
Thanks for your feedback