Improving the front-end size of a hosting panel

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.

Generated by the `source-map-explorer` package. Full image: http://i.imgur.com/1tGpl4K.png

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/
  )
 ]
};





This is how it works.

(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.

Report Page