Progressive Web Apps?
One of the good solutions for better UX is Progressive Web App. Google this term if you don't know it yet. There are tons of good posts and videos about it. So Progressive Web has several core ideas, but right now I want to focus on Progressive Loading and implement it .
The idea of Progressive Loading is very simple:
- Make "initial load" as fast as possible.
- Load UI components only when they are required.
Let us assume we have React Application that draws some charts on a page:
Chart components are very simple:
Please note that LineChart and BarChart are not visible on the first load. To see them a user needs to toggle checkbox:
So it is possible that the user will NEVER toggle that checkbox. And this is a very common situation in real world web application: when a user never opens some parts of the app (or open them later). But with a current approach, we have to bundle all components and all their dependencies into one file. In this example we have: root App component, React, Chart components, react-konva, konva.
280kb for bundle.js and 3.5 seconds for initial loading with a regular 3g connection.
Implementing Progressive Loading
How can we remove these chart components from bundle.js and load them later and draw something meaningful as fast as possible? Say hello to good old AMD (asynchronous module definition)! And webpack has good support for code splitting.
I suggest to define HOC (higher order component) that will load chart only when a component is mounted into DOM (with componentDidMount lifecycle callback). Let's define LineChartAsync.js:
Then instead of
import LineChart from './LineChart';
We should write:
import LineChart from './LineChartAsync';
Let us see what we have after bundling:
We have bundle.js that includes a root App component and React.
1.bundle.js and 2.bundle.js are generated by webpack and they include LineChart and BarChart . But, wait, why is the total sum bigger? 143kb+143kb+147kb = 433kb vs 280kb from previous approach. That is because dependencies of LineChart and BarChart are included TWICE ( react-konva and konva defined in both 1.bundle.js and 2.bundle.js), we can avoid this with webpack.optimize.CommonsChunkPlugin:
// (use all children of the chunk)
// (create an async commons chunk)
Now dependencies of LineChart and BarChart are moved in another file 3.bundle.js, total size is almost the same (289kb):
Now 1.75 seconds for initial loading. It is much better then 3.5 seconds.
To make the code better I would like to refactor LineChartAsync and BarChartAsync. First, let's define basic AsyncComponent:
And BarChartAsync (and LineChartAsync)can be rewritten into simpler component:
But we can improve Progressive Loading even more! When application is initially loaded we can schedule loading of additional component on background, so it is possible that they will be loaded before user toggled checkbox
And loader.js will be something like this:
Also, we can define components that will visible on the first screen, but in fact loaded asynchronously later and a user may see beautiful placeholder while a component is loading. Please note that placeholder is not for API call. It is exactly for loading component's module (its definition and all its dependencies).
As a result of all improvements:
- Initial bundle.js has a smaller size. That means a user will see some working UI components faster.
- Additional components can be loaded asynchronously in the background.
- While a component is loading it can be replaced with some placeholder components .
- For exactly this approach Webpack is required. But you can use it not only with React, but with other frameworks too.
Take a look https://github.com/lavrton/Progressive-Web-App-Loading for full source and webpack configurations.