Improve React Performance using Lazy LoadingšŸ’¤ andĀ Suspense

December 31, 2018 0 Comments

Improve React Performance using Lazy LoadingšŸ’¤ andĀ Suspense

 

 

Lazy loading💤 has come to be one of the optimization techniques widely used now to speed up the load time. The prospect of Lazy Loading helps reduce the risk of some of the web app performance problems to a minimal〽.

In this article, we will look into how to use lazy loading💤 to optimize load time in our React apps.

Lazy loading is an optimization trick💫 whereby we delay the load of an object(image🎦, video🎬, webpage🌎, music file🎶, documents📋) until when it is needed.

When a user opens a webpage, all the contents are downloaded in one fell swoop. Most of the contents may never be interacted with or seen by the user. So why waste precious resources and bandwidth?

To boost the response time of our site, we defer the loading of some non-critical parts of our app at load time. These resources are then loaded when the user accesses part of the page that requires it.

We have SSR(server-side rendered) apps and CSR(Client side rendered) apps.

SSR is the good old web pages built with .HTML and inlined with ASP.NEt, PHP. Each link has a different .HTML file to load

web-app/
- index.html
- about.html
- faq.html
- careers.html

Each page has a different HTML file.

As JS frameworks emerged, the web pages are mashed up into a single js and loaded in one fell swoop. Upon execution in the browser, the requested page is generated by the browser DOM.

In React, let’s say we have an app like this:

// index.js
export default () =>
<Switch>
<Route path='/about' component={About} />
<Route path='/faq' component = {FAQ} />
<Route path='/careers' component = {Careers} />
</Switch>
// about.js
class About extends Component{
render() {
return <div>About page</div>
}
}
// faq.js
class FAQ extends Component{
render() {
return <div>FAQ page</div>
}
}
// careers.js
class Careers extends Component{
render() {
return <div>Careers page</div>
}
}

On bundling, webpack packs all the js files into one index.js

react-app/
dist/
- index.html
- index.js

All files index.js, about.js, faq.js, careers.js was bundled into one file. Now, when we load the index.html file it drags along the heavily-laden index.js with it. Now, the time⏰ taken to parse through all the codes in the index.js and rendering will be a long wait. If the time taken for each js file to execute is as follows:

  • index.js 2ms
  • about.js 10ms
  • faq.js 5ms
  • careers.js 9ms

Bundled:

Therefore we will wait for 26ms!! But if we could separate the files in React and load them on demand, we will see that it will take our app 2ms to load and respond.

So instead of downloading the entire code, we can split the bundle into small chunks and dynamically load them at runtime.

So many techniques have been used to code-split React apps. We see them in the next sections.

To code-split, our JS apps, the import() function was introduced. It’s still a proposal, not yet part of JavaScript standard.

This function makes it possible to split our apps into chunks and load them on demand.

The import() takes a string as a parameter. The string is the path of the js file to load.

import('./jsfileto_load.js')

When webpack comes across this, it bundles the file in the path separately.

React.lazy is a new feature added to React when Reactv16.6 was released, it offered an easy and straight-forward approach to lazy-loading and code-splitting our React components.

The React.lazy function lets u render a dynamic import as a regular component. — React blog

React.lazy makes it easy to create components and render them using dynamic imports. React.lazy takes a function as a parameter:

React.lazy(()=>{})
// or
function cb () {}
React.lazy(cb)

This callback function must load the component’s file using he dynamic import() syntax:

// MyComponent.js
class MyComponent extends Component{
render() {
return <div>MyComponent</div>
}
}
const MyComponent = React.lazy(()=>{import('./MyComponent.js')})
function AppComponent() {
return <div><MyComponent /></div>
}
// or
function cb () {
return import('./MyComponent.js')
}
const MyComponent = React.lazy(cb)
function AppComponent() {
return <div><MyComponent /></div>
}

The callback function of the React.lazy returns a Promise via the import() call. The Promise resolves if the module loads successfully and rejects if there was an error in loading the module, due to network failure, wrong path resolution, no file found, etc.

When webpack walks through our code to compile and bundle, it creates a separate bundle when it hits the React.lazy()and import(). Our app will become like this:

react-app
dist/
- index.html
- main.b1234.js (contains Appcomponent and bootstrap code)
- mycomponent.bc4567.js (contains MyComponent)
/* index.html /
<head>
<div id="root"></div>
<script src="main.b1234.js"></script>
</head>

Now, our app is now separated into multiple bundles. When AppComponent gets rendered the mycomponent.bc4567.js file is loaded and the containing MyComponent is displayed on the DOM.

Now, what happens when the file mycomponent.bc4567.js gets loaded, there must be a time lag from when it loaded to when the MyComponent it contains is rendered. What will the user see?

Apparently, your app will seem to freeze for some time. That will be a bad user experience. We need to let the user know that something is happening or loading. In order to do that a new feature associated to React.lazy was added it is the Suspensecomponent.

The Suspense component is used to wrap lazy components to show some fallback content while loading the lazy components.

const Lazycomponent = React.lazy(()=>import('./lazy.component.js'))
function AppComponent() {
return
<div>
<Suspense fallback={<div>loading ...</div>}>
<LazyComponent />
</Suspense>
</div>
}

The component that is being lazily loaded is inserted inside the Suspense component tags. The content to show to the user to tell them that something is going on is placed in the fallback prop of the Suspense component tag.

Components can also be used in the fallback prop:

// ...
function LoadingIndicator () {
return <div>loading ...</div>
}
function AppComponent() {
return
<div>
<Suspense fallback={<LoadingIndicator />}>
<LazyComponent />
</Suspense>
</div>
}

Multiple lazy components can be placed in the Suspense tag.

const Lazycomponent1 = React.lazy(()=>import('./lazy.component1.js'))
const Lazycomponent2 = React.lazy(()=>import('./lazy.component2.js'))
const Lazycomponent3 = React.lazy(()=>import('./lazy.component3.js'))
const Lazycomponent4 = React.lazy(()=>import('./lazy.component4.js'))
function AppComponent() {
return
<div>
<Suspense fallback={<div>loading ...</div>}>
<LazyComponent1 />
<LazyComponent2 />
<LazyComponent3 />
<LazyComponent4 />
</Suspense>
</div>
}

In our React.lazy, we mentioned that the import() function returns a Promise, that rejects on loading error due to some reasons:

  • network failure
  • file not found
  • file path error

Now, we don’t want our app to fail miserably. We would like to fail with honor and dignity. To show a nice user experience upon failure, we will place an error boundary above the lazy components.

const Lazycomponent1 = React.lazy(()=>import('./lazy.component1.js'))
const Lazycomponent2 = React.lazy(()=>import('./lazy.component2.js'))
const Lazycomponent3 = React.lazy(()=>import('./lazy.component3.js'))
const Lazycomponent4 = React.lazy(()=>import('./lazy.component4.js'))
import ErrorBoundary from './error.boundary.js'
function AppComponent() {
return
<div>
<ErrorBoundary>
<Suspense fallback={<div>loading ...</div>}>
<LazyComponent1 />
<LazyComponent2 />
<LazyComponent3 />
<LazyComponent4 />
</Suspense>
<ErrorBoundary/>
</div>
}

Knowing where to split your code can be very tricky. Two of the most commonly used methods are route-based and component-based.

Route-based code splitting breaks down an application into chunks per route.

Route-based code splitting is the splitting of your code into bundles that are related to your app’s routes. In SPAs, all route or path is done on the DOM. When u click on a hyperlink the DOM catches the event and runs it through the SPA framework, the current view in the DOM is destroyed while the Component attached to the requested path is created and rendered.

All these components are bundled together in one file and shipped. Now, with route-based chunking, we can split the code into chunks. Each chunk relating to a particular route.

// index,js
// ...
const App = () =>
<Switch>
<Route path='/about' component={About} />
<Route path='/faq' component = {FAQ} />
<Route path='/careers' component = {Careers} />
</Switch>

When we split this app using the route-based method we will get this:

react-app/
- index.html
- index.bacd0123.js (contains App)
- about.1234.js (contains About component)
- faq.5678.js (contains FAQ component)
- careers.9012.js (contains Careers component)
/ index.html */
<head>
<div id="root"></div>
<script src="index.bacd0123.js"></script>
</head>

When we load our app /. The App is only rendered. Now, if we navigate to /faq. The faq.5678.js file is fetched via a network and loaded, the containing component, FAQ is rendered.

So, we see that our code was split based on the routes we defined. This doesn’t optimize our app 100%, there are downsides though. But at least we shaved off a sizeable delay time from our app. This another optimization technique associated with code-splitting called prefetching, but that will be in another post.

In a web application, there are so many widgets like:

  • modals
  • tabs
  • accordion
  • progressbars
  • sidenav
  • footer
  • header
  • panel
  • etc

These widgets or components provide a rich experience to our users.

function ModalComponent() {
return <modal>Modal shows!!!</modal>
}
function Mycomponent() {
this.display = false;
return
<div>
<ModalComponent display={this.dispaly} />
<button onclick={this.display = true}>Open Modal</button>
<button onclick={this.display = false}>Close Modal</button>
</div>
}
react-app/
dist/
- index.html
- index.js

In as much as they provide many goodies, they also contribute to the poor performance of our apps. Most of the components hide until when the user does something to display it. The footer and the sidenav are never seen until the user either scrolls to the bottom or press the sidenav button. Most cases, the user might not even interact with any of them.

Tip: Use Bit to vastly improve your work with components, easily share and reuse components, and create a visual component design system. Try it!

All these are loaded in our app and constitute a time delay on loading. Wouldn’t it be nice to shave off the widgets and load them when the user tries to interact with them?

In this case, component-based splitting helps a lot. All the widgets or components will be bundled separately. Each widget on a separate bundle. It’s up to the web dev to come with a bundling technique but the main thing is to load the widgets on demand.

// modalcomponent.js
function ModalComponent() {
return <modal>Modal shows!!!</modal>
}
// mycomponent.js
const ModalComponent = React.lazy(import('./modalcomponent.js'))
function MyComponent() {
this.display = false;
return
<div>
<Suspense fallback={<div>loading ...</div>}>
<ModalComponent display={this.display} />
</Suspense>
<button onclick={this.display = true}>Open Modal</button>
<button onclick={this.display = false}>Close Modal</button>
</div>
}
// index.js
const MyComponent = React.lazy(()=>import('./mycomponent.js'))
import ErrorBoundary from './error.boundary.js'
function AppComponent() {
return
<div>
<ErrorBoundary>
<Suspense fallback={<div>loading ...</div>}>
<MyComponent />
</Suspense>
<ErrorBoundary/>
</div>
}
react-app/
dist/
- index.html
- index.js
- mycomponent.js
- modalcomponent.js (contains the ModalComponent)

We see that the ModalComponent is not loaded initially. It is loaded when the user clicks the Open Modal button.

In this post, we saw how we could improve🚀 the performance of our React app by code-splitting and lazy-loading. First, we introduce the dynamic import() function, next, we looked at different techniques using the new 💤React.lazy() and 🚦Suspense.

With these different optimization tricks we learned in this post, we can churn out quality React apps with high performance.

If you have any question regarding this or anything I should add, correct or remove, feel free to comment below and ask me anything! 👏 You can also find me on Twitter, Facebook and always here.

Thanks !!!💯


Tag cloud