Improving the front-end size of a hosting panel
Ivan Akulov (@iamakulov)In May 2017, I helped the Brainex team to optimize a front-end app for a major hosting provider. This is a shortened version of the analysis I performed. We’re planning to implement these changes when we find the time.
Prerequisites: The app is written with React and built with webpack. Build produces one .js
file and a bunch of .ttf
files.
Analyzing dependencies
Let’s take a look into the bundle.
These are the 10 largest libraries:
- parse5
- moment
- iconv-lite
- chart.js
- react-dom
- mime-db
- redux-form
- immutable
- lodash
- core-js
We can optimize at least 5 of them.
parse5
This is the largest library that takes 220 kB of minified code. And we can remove it!
parse5
is required by the react-render-html
library. And react-render-html
is required by the client code in src/common/localization/utils.js
. This is how it’s used:
getMessage
is a function used for localization. Usually, it accepts a key and return the corresponding translation. However, if it receives the html: true
parameter, it renders the translation into into JSX (using the react-render-html
library) and returns the JSX block.
Suggestion. We can replace the renderHTML
call with the code like this:
message = <span dangerouslySetInnerHTML={{__html: message}} />
This will make react-render-html
unnecessary.
Important point. This solution is not bulletproof because message
can sometimes contain block tags like p
or ul
, which, being wrapped into span
, could sometimes shift in the document. To make sure it’s is working, I’d suggest to visually check all the places where getMessage
is used with the html
option (thankfully, it’s a rare case) and verify that the design looks as it should do. If necessary, we can introduce an API to allow replacing span
with another tag.
moment
Moment is so large because it includes all the localization files by default.
Suggestion. As the project only uses the Russian localization, we can strip all the other localizations with ContextReplacementPlugin
:
// webpack.config.js const webpack = require('webpack'); module.exports = { plugins: [ new webpack.ContextReplacementPlugin( /moment[\/\\]locale/, /ru\.js/ ) ] };
(The en-US localization is embedded directly into Moment.)
chart.js
The library is required only by the Data
component in src\containers\Resources\Data\Data.js
. This component uses only the Pie
type of chart.
Suggestion. The largest optimization here would be to drop the unused chart types.
With Moment, we used ContextReplacementPlugin
because it imported locales dynamically. With Chart.js, we should use IgnorePlugin
because its imports are static.
This is how Chart.js imports chart types:
require('./controllers/controller.bar')(Chart); require('./controllers/controller.bubble')(Chart); require('./controllers/controller.doughnut')(Chart); require('./controllers/controller.line')(Chart); require('./controllers/controller.polarArea')(Chart); require('./controllers/controller.radar')(Chart); require('./charts/Chart.Bar')(Chart); require('./charts/Chart.Bubble')(Chart); require('./charts/Chart.Doughnut')(Chart); require('./charts/Chart.Line')(Chart); require('./charts/Chart.PolarArea')(Chart); require('./charts/Chart.Radar')(Chart); require('./charts/Chart.Scatter')(Chart);
To remove the unused chart types, we mark the modules we don’t as ignored. This could be achieved with this code:
// noop.js module.exports = () => {}; // webpack.config.js const webpack = require('webpack'); const path = require('path'); module.exports = { plugins: [ new webpack.IgnorePlugin( /controller\.(bar|bubble|<...>)/, /chart\.js[\/\\]src[\/\\]controllers/, ), new webpack.NormalModuleReplacementPlugin( /chart\.(Bar|Bubble|<...>)/, /chart\.js[\/\\]src[\/\\]charts/, ), ] };
Important point. This change should be kept in mind – if, at some moment, we try to use another type of chart, we’ll get a runtime error. As this change will only drop ≈20-22 kB of minified code, it’d make an additional consideration before implementing it.
lodash
Suggestion. We can optimize Lodash by dropping the unused methods and keeping only the used ones. This can be achieved automatically with babel-plugin-lodash
and optionally lodash-webpack-plugin
.
core-js
This library is imported via babel-polyfill
.
Suggestion. We’re supporting IE10+ so we can’t drop a lot of Core.js modules (if we can any). We can, however, replace babel-polyfill
with a service like cdn.polyfill.io which serves polyfills based on the User-Agent of the browser. For users of modern browsers, this will decrease the size of the loaded JavaScript by 40+ kB.
Can’t be optimized (?)
iconv-lite
It’s a subdependency of node-gettext
which is used in Provider.js
for localization purposes.
gettext
is tightly bound with our localization system, so I wouldn’t spend much time here because any deep changes would be expensive.
react-dom
Most likely, can’t be optimized.
As an option, we can replace React with Preact or Inferno. However, this is a major change that should be carefully considered.
mime-db
This library is a JSON of all the existing mime types. It’s used by mime-types
which is imported by Filemanager.js
:
import {lookup} from 'mime-types'; // ... download(response, file.get('name'), lookup(file.get('name')));
A generic method is used for a generic purpose. Most likely, this can’t be optimized.
redux-form
This library looks pretty monolithic, and is used in a variety of ways, so, most likely, it can’t be optimized.
immutable
The library is served as a single file, and, like redux-form, it’s used pretty often, so I believe we can’t replace the library or rem0ve some of its code.
Analyzing the app loading
Note: this part of the analysis is less detailed because it’s expected that I will do these changes. Performing a deep analysis beforehand was unnecessary at the moment of writing.
The flow
Let’s take a look at the loading process with the disabled cache.
First 7 requests (bootstrap.js
...styles.css
) are the app code. Then, analytics gets loaded. Then, the app starts sending requests for data.
I see several things that could be optimized here:
main.js
is huge (5.5 MB). On my connection, its download takes 7s. That’s very long.styles.css
are loaded after the JS code. Ideally, they should come earlier.analytics.js
blocks data retrieval. It shouldn’t.
I’ll focus on the first point since it has the largest potential for optimizations.
There’re several problems with main.js
. It’s huge, it’s slow to download and parse (especially on mobiles), it’ll be re-downloaded after each release (once a week). To fix all these issues, we should split it into smaller files.
I suggest to do the following:
- Split the app by routes. The app uses React-Router 3, and it makes code splitting easy. Splitting the app will both decrease the amount of code that’s loaded in the beginning + improve the caching.
- Moving some common code (like libraries that are used throughout the whole app) into a common file. This can be done with
CommonsChunkPlugin
.
Cache headers
Everything’s OK.