Getting high Progressive Web App score on React and Material-UI

Denis Degtiarev
DailyJS
Published in
5 min readSep 24, 2017

--

Image by Outware

The fast and progressive web application is a must-have standard for recent front-end development. Meet both progressiveness and performance sometimes become not so easy. Single page application usually comes with a heavy JavaScript code bundle. Client’s device has to load it before the application can be interactive.

Google tries to help developers estimate application performance providing Lighthouse utility. It can be found on Audits tab of Google Chrome developer tools panel. Below are some basic techniques I have used to get a high score for example application based on a stack of React, Redux and Material-UI.

Split your code bundle

Many web bundling tools like Grunt, Gulp and so can do that job. Webpack does it in a more elegant and simple way providing many features and techniques.

First, separate heavy vendor code to the vendor file:

entry: {
app: ['./src/bootstrap.js'],
vendor: [
'react',
'react-dom',
'redux',
'react-redux',
'redux-thunk',
'react-router-dom',
'prop-types',
'jss',
'axios',
],
},

To understand which libraries are candidates to re-settle try great webpack-bundle-analyzer package. We also have to say webpack to collect vendor data from assets:

plugins: [
...
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'js/vendor.js',
minChunks: Infinity,
}),
...
],

Then, move independent components to the separate chunk files. You can define new webpack entries for them or include them in your code through bundle-loader plugin:

import SignIn from 'bundle-loader!user/containers/SignIn';

Finally, we can ask webpack to find a common code in our chunks and separate it as well:

plugins: [
...
new webpack.optimize.CommonsChunkPlugin({
name: 'meta',
filename: 'js/meta.js',
minChunks: 2,
}),
...
],

Use lazy loading

Lazy loading is one of the beneficial techniques used for performance optimisation. Our chunks, included by bundle-loader, will be loaded on a router initialization. We can use a trick to lazy load them on demand. Instead of pointing out route to chunk component we can point it out to bundle wrapper component:

import { Component } from 'react';
import PropTypes from 'prop-types';

class BundleComponent extends Component {
static propTypes = {
loader: PropTypes.func.isRequired,
children: PropTypes.func.isRequired,
};

state = {
mod: null,
};

componentWillMount() {
this.load(this.props);
}

componentWillReceiveProps(nextProps) {
if (nextProps.loader !== this.props.loader) {
this.load(nextProps);
}
}

load = ({ loader }) => {
this.setState({
mod: null,
});
loader((mod) => {
this.setState({
mod: mod.default ? mod.default : mod,
})
});
};

render() {
return this.state.mod
? this.props.children(this.state.mod)
: <span />;
}
}

export default BundleLoader => props => (
<BundleComponent loader={BundleLoader}>
{LoadedComponent => <LoadedComponent {...props} />}
</BundleComponent>
);

Then we can use bundle wrapper to initialize router:

import bundle from './bundle';import RegLoader from 'bundle-loader?lazy!user/containers/Registration';
import SignInLoader from 'bundle-loader?lazy!user/containers/SignIn';
const AppRoutes = () => (
<Router>
<App>
<Route path='/register' component={bundle(RegLoader)} />
<Route path='/login' component={bundle(SignInLoader)} />
</App>
</Router>
);

It is also a good idea to load component’s content after rendering component itself:

import AppBarContentLoader from 'bundle-loader?lazy!appbar/components/AppBarContent';
const AppBarContent = bundle(AppBarContentLoader);

const Layout = ({ children }) => {
return (
<div className='wrapper'>
<AppBar>
<AppBarContent />
</AppBar>

<div className='content'>
{children}
</div>
</div>
);
};

Use JSS

Single page application has to load static CSS before initial rendering. That means we have to load more than 100 Kb, increasing the initial time of interaction. We can do better with JSS including only necessary styles only when they are actually needed:

import { withStyles } from 'material-ui/styles';

const styleSheet = theme => ({
root: {
display: 'flex',
minHeight: '100vh',
},
content: {
flex: '1 1 100%',
margin: '0 auto',
},
appBar: {
minHeight: 56,
},
});

const Layout = ({ children, classes }) => {
return (
<div className={classes.root}>
<AppBar className={classes.appBar}>
<AppBarContent />
</AppBar>

<div className={classes.content}>
{children}
</div>
</div>
);
};

export default withStyles(styleSheet)(Layout);

Implement Service Worker

Service Worker is a light and powerful middleware between application and server. It uses Javascript and allows you to control any request and response. For us, it means we can control which assets can be cached, when and why. As a further application, we can handle requests to a server when a network is not available. Then send these requests when the application goes online.

First of all, let’s define example caching strategy we would like to implement:

  • store only GET requests to our server;
  • exclude requests to API to get actual data;
  • hash static assets (CSS, js, fonts, etc.) by content and store them to permanent storage;
  • store HTML responses to temporary storage until the next build of the application.

Getting static asset names hashed by content can be done by Webpack:

output: {
path: path.join(__dirname, '/public/'),
filename: 'js/[name].[chunkhash].js',
chunkFilename: 'js/[name].[chunkhash].js',
publicPath: '/',
},

In that case, we haven’t track file updates, if the file is not in the cache we have to cache it:

// Hash: {hash}
var PERMANENT_STORAGE = 'react-pwa-perm';
var TEMP_STORAGE = 'react-pwa-temp';
var permanentUrlTemplates = [
'/js/',
'/fonts/',
'/styles/'
];
var urlsToInstall = ['{files_to_cache}'];
var hostnameToCache = '{hostname}';
var hostnameExcludeFromCache = '{api_hostname}';

self.addEventListener('fetch', function(event) {
if (event.request.method !== 'GET' ||
event.request.url.indexOf(hostnameToCache) === -1 ||
event.request.url.indexOf(hostnameExcludeFromCache) !== -1
) {
return;
}
event.respondWith(
caches.match(event.request)
.then(function(response) {
if (response) {
return response;
}

var fetchRequest = event.request.clone();

return fetch(fetchRequest).then(
function(response) {
if(!response || response.status !== 200 || response.type !== 'basic') {
return response;
}

var responseToCache = response.clone();

var isPermanent = false;
permanentUrlTemplates.map(function(template) {
if (event.request.url.indexOf(template) !== -1) {
isPermanent = true;
}
});

caches.open(isPermanent
? PERMANENT_STORAGE
: TEMP_STORAGE
).then(function(cache) {
cache.put(event.request, responseToCache);
});

return response;
}
);
})
);
});

On first initial loading, service worker has to be installed. To do it right we have to provide a list of resources to cache. Service worker registers its instance only on its success caching. But our assets are hashed and we can get hashed file names only on a webpack building stage. To help us inject hashed assets we can use inject-assets-webpack-plugin:

const InjectAssetsWebpackPlugin = require('inject-assets-webpack-plugin');const webpackConfig = {
...
plugins: [
...
new InjectAssetsWebpackPlugin({
filename: 'public/worker.js',
},[
{ pattern: '{hash}', type: 'hash' },
{ pattern: '{hostname}', type: 'value', value: config.app.hostname },
{ pattern: '{api_hostname}', type: 'value', value: config.app.serverUri },
{
pattern: '{files_to_cache}',
type: 'chunks',
chunks: ['app', 'vendor', 'meta', 'AppBarContent'],
files: ['.js', '.css'],
excludeFiles: ['.map'],
decorator: fileNames => fileNames.join('\', \''),
},
]),
...
],
...
};

Conclusion

Building modern and progressive web applications become simple if we try to do it right. Sticking with techniques observed upper can give us PWA score close to a maximum:

Hope this article can help someone to save hours of finding a right way to build Progressive Web App. A source code of a complete example available on my GitHub. A working example can be found at altbit-dev.co.uk.

Appreciate your support. Feel free to write me anytime.

--

--