⬆️ ⬇️

Progressive loading of a web application using code sharing

In this article we will look at how you can speed up the initial loading of a web application using code splitting. To implement my plans, I will use webpack v1 , and for demonstration, React (optional).



In most of my projects, I collect all the javascript files (and sometimes css and images too) into ONE VERY BIG bundle.js . Perhaps you, dear reader, do the same. This is a fairly standard practice for modern web applications.



But this approach has one (and sometimes quite important) drawback: the initial download of the application can take a very long time, since the web browser must (1) load a huge file and (2) parse a ton of js code. Downloading a file can take a long time if the user has slow internet. Also, this huge file may contain code for components that the user will NEVER see (for example, the user will simply not open some parts of your application).

')

What to do?



Progressive download



One solution for a better UX is the Progressive Web App . If the term is not familiar, I suggest it to google it quickly, you can find a lot of good videos and articles. So Progressive Web App contains a lot of interesting ideas, but now I want to focus only on Progressive Loading .



The idea of ​​progressive download is quite simple:

1. Make the initial download as fast as possible.

2. Load UI components only as needed.



Suppose we have some React application that draws a graph on the page:



// App.js import React from 'react'; import LineChart from './LineChart'; import BarChart from './BarChart'; export default class App extends React.Component { //       state = { showCharts: false }; //     handleChange = () => { this.setState({ showCharts: !this.state.showCharts }); } render() { return ( <div> Show charts: <input type="checkbox" value={this.state.showCharts} onChange={this.handleChange} /> { this.state.showCharts ? <div><LineChart/><BarChart/></div> : null } </div> ); } } 


The component for drawing graphics is very simple:



 // LineChart.js import React from 'react'; import {Stage, Layer, Line} from 'react-konva'; export default () => ( <Stage width={100} height={100}> <Layer> <Line stroke="green" points={[0, 0, 20, 90, 50, 20, 100, 100]}/> </Layer> </Stage> ); 




 // BarChart.js import React from 'react'; import {Stage, Layer, Rect} from 'react-konva'; export default () => ( <Stage width={100} height={100}> <Layer> <Rect fill="red" width={20} height={20}/> <Rect fill="blue" x={50} width={20} height={60}/> </Layer> </Stage> ); 


Such graphics can be very heavy. In this example, each of them has react-konva dependencies (as well as konva , like the react-konva dependency ).



Please note that the LineChart and BarChart charts are not visible on first boot. In order for the user to see them, he needs to check the checkbox:



image



Perhaps the user will never click checkbox. And this is quite a common situation in real and large applications, when the user does not access all parts of the application or opens them only after some time. Then with the current approach we compile all the dependencies in one file. In this case, we have: the root component App, React, components of graphs, react-konva, konva.



Collected and minified result:



image



Network usage at boot time:



image



280kb for bundle.js and 3.5 seconds for the initial download on a 3g connection.



Progressive download implementation



How can I remove graph components from budle.js and load them later, thereby making the initial load much faster? Say hello to the good old AMD (asynchronous module definition)! Webpack also has good support for code splitting.



I propose to implement a HOC (higher order component, also known as a higher order component) that will load the schedule code only when the component is installed in the DOM (using componentDidMount ):



 // LineChartAsync.js import React from 'react'; export default class AsyncComponent extends React.Component { state = { component: null } componentDidMount() { //      DOM require.ensure([], (require) => { // !!       : // require(this.props.path).default; // ,  webpack       //       const Component = require('./LineChart').default; this.setState({ component: Component }); }); } render() { if (this.state.component) { return <this.state.component/> } return (<div>Loading</div>); } } 


Further, instead of writing:



 import LineChart from './LineChart'; 


Will write:



 import LineChart from './LineChartAsync'; 


Let's see what we have after assembly:



image



We have bundle.js, which contains the App and React components.



The files 1.bundle.js and 2.bundle.js are generated by the webpack and include LineChart and BarChart. But wait, why the total file size has become larger? 143kb + 143kb + 147kb = 433kb. In the previous approach there was only 280kb. This is because the LineChart and BarChart dependencies are included TWICE (react-konva and konva are defined in 1.bundle.js and 2.bundle.js). We can fix this with webpack.optimize.CommonsChunkPlugin :



 new webpack.optimize.CommonsChunkPlugin({ children: true, async: true, }), 


So we get:



image



Now the LineChart and BarChart dependencies are moved to a separate 3.bundle.js file, and the total size remains almost the same - 289kb:



Using the network during the first boot:



image



Network use after displaying graphs:



image



Now we have 1.75 seconds for the initial download. This is already much better than 3.5 seconds.



Refactoring



To make the code somewhat better, I suggest rewriting LineChartAsync and BarChartAsync a bit . First we define the basic AsyncComponent component:



 // AsyncComponent.js import React from 'react'; export default class AsyncComponent extends React.Component { state = { component: null } componentDidMount() { this.props.loader((componentModule) => { this.setState({ component: componentModule.default }); }); } renderPlaceholder() { return <div>Loading</div>; } render() { if (this.state.component) { return <this.state.component/> } return (this.props.renderPlaceholder || this.renderPlaceholder)(); } } AsyncComponent.propTypes = { loader: React.PropTypes.func.isRequired, renderPlaceholder: React.PropTypes.func }; 


Further BarChartAsync (and LineChartAsync) can be rewritten into simpler components:



 // BarChartAsync.js import React from 'react'; import AsyncComponent from './AsyncComponent'; const loader = (cb) => { require.ensure([], (require) => { cb(require('./BarChart')) }); } export default (props) => <AsyncComponent {...props} loader={loader}/> 


But we can STILL improve progressive loading. Once the application is initially loaded, we can load additional components in the background. They may be loaded before the user tick the checkbox.



 // BarChartAsync.js import React from 'react'; import AsyncComponent from './AsyncComponent'; import sceduleLoad from './loader'; const loader = (cb) => { require.ensure([], (require) => { cb(require('./BarChart')) }); } sceduleLoad(loader); export default (props) => <AsyncComponent {...props} loader={loader}/> 


And loader.js will look something like this:



 const queue = []; const delay = 300; let isWaiting = false; function requestLoad() { if (isWaiting) { return; } if (!queue.length) { return; } const loader = queue.pop(); isWaiting = true; loader(() => { setTimeout(() => { isWaiting = false; requestLoad(); }, delay) }); } export default function sceduleLoad(loader) { queue.push(loader); requestLoad(); } 


Also, we can now determine the components that will be visible after the full initial load, but in fact are loaded asynchronously in the background and the user will see a beautiful progress bar while the component is loading.



image



I note that this progress bar was not made to call the API, but to load the module itself (its code and its dependency code).



 const renderPlaceholder = () => <div style={{textAlign: 'center'}}> <CircularProgress/> </div> export default (props) => <AsyncComponent {…props} loader={loader} renderPlaceholder={renderPlaceholder} /> 


Conclusion



As a result of our improvements, we get:



1. The initial bundle.js is smaller. This means that the user will see on the screen something meaningful much earlier;

2. Additional components could be loaded asynchronously in the background;

3. While the room is loading, we can show a beautiful stub or progress bar so that the user does not get bored and see the download process;

4. For the exact same implementation, you will need a webpack. I used React as an example, this solution can also be used with other frameworks / libraries.



The full source code of the sample and configuration files can be found here: https://github.com/lavrton/Progressive-Web-App-Loading .

Source: https://habr.com/ru/post/307694/



All Articles