PWA @ Scale

First load, then load, then ...

When building traditional, native apps, your users first and foremost have to install them, which most of the time will translate to a bulk download. Once locally fully available, the application can be started and no more loading delays data-wise have to be made - every view that will be shown is already "there", e.g. on disk and ready to be loaded into memory.

That's nice, until a better solution arises. Enter PWAs with React.

From Zero to Infinity

One of modern web apps' absolute hallmarks and advantage over traditional native apps is Javascript's JIT (just-in-time) compiler, which doesn't need to know everything about the application it's going to load at runtime.

As a side effect, this means that we can delay loading code until it's actually needed. And by loading code I mean actually fetching the code from somewhere, e.g. your backend, and only then loading it into memory.

This feature is called code-splitting and used to great extent with React's Suspense for our views. When building a modern PWA with the React framework, we can decide to defer loading React components until they are actually required to be rendered. This is known as lazy-loading your views. Let's take a look at a very simple example:

import React, { lazy, Suspense } from "react";

// Store promised components.
// No view gets actually loaded.
const User = lazy(() => import("./User"));
const Settings = lazy(() => import("./Settings"));

// Return our container component,
// which itself returns a view based
// on the prop it receives.
export default function({ selected }){
 if(selected === "user"){
   return <Suspense fallback=""><User/></Suspense>;
  if(selected === "settings"){
   return <Suspense fallback=""><Settings/></Suspense>;
 // Nothing selected, so return null.
 return null;

Just Promises

Based on the dynamic imports provided with ES6 (the current stable version of Javascript) via import(), React provides the lazy-function to wrap our component-imports with.

What we get is not the loaded component, as it happens with standard imports, but rather a reference to the view we want to load and render in the future, but not right now.

As shown in the example, to fully use the lazy view import, we also need to mount the component as child of Suspense, a special React component that shows a fallback until the code for the lazy view has been loaded. When fully available, the component gets mounted as every other view.

To recap: React components imported via lazy() are only then actually loaded when mounted as a child of Suspense. On a side note, you can mount any number of lazy-views as children of a single Suspense.

Being lazy means being efficient

This opens up a new paradigm in how to handle your views! No matter how many you have, as long as a branch of your view-tree isn't needed, the code required won't even be loaded! Effect: Your app's initial view can load a few children "the classic way" and defer other, top-level components until they are used - e.g. containers of a tab bar, drawer or else.

Yet this doesn't mean that going all-in and lazy-loading everything is a good solution. Drawbacks included are first and foremost some kind of waiting screen for your user (even if you only show a blank screen for some hundred ms).

Delivering your app's views at lightspeed is therefore a balancing act between loading more of them at once or delaying some others until actually required. Good planing and testing is key here! The larger a component, the more likely it is you want to defer its loading.

Note that using the latest ES6-features, including import() in all browsers requires the setup of a build-chain. As we're building on the shoulders of giants, I recommend CRA (create-react-app) to init your app, which in turn uses react-scripts.

And that was my wrap-up. I hope you enjoyed this quick demonstration of how to defer loading parts of your app until needed, thus reducing the inital loading time to a minimum!

- Tom

Reach out to us! We are hiring!

expressFlow is now Lean-Forge