While developers create apps, they may stumble upon content loading issues at some point. They may also inherit a codebase which was never checked against performance.
Fortunately, there are many ways to make it faster and they are not necessarily too time-consuming.
We won’t cover simple tricks here such as removing unused but still downloaded content, as it’s obvious.
Our team at Brainhub was tasked with something that seemed like a trivial errand – we were to create a few moderately large React.js components that would be inserted into a working site run by a traditional approach of PHP + jQuery.
When we were about to finish our first major iteration, we and our client noticed that the site took at least 12s to get interactive. React.js is very fast so there shouldn’t have been a problem with that, but we checked it anyway. The whole JS bundle downloaded and parsed swiftly, so as we suspected the problem must have lied somewhere else.
As we started getting deep into the existing code, we discovered many quirks, hacks, and optimizations along the way.
In the end, our app took less than 3s from the moment the document loaded. This article is the achievement of our work.
Webpack provides us with many built-in features such as dead code elimination. It imports only necessary code and ignores all the rest. The lesser code size, the better.
React.js, Angular, Vue etc. are client-side frameworks; that means all content will be generated in the browser, instead of receiving a plain document object from the server. This isn’t a bad thing as modern browsers’ engines are very performant, but it creates a problem in certain areas:
- Disabled people who rely on screen readers are much better off with static content.
- Lightweight devices with short-span battery lives.
- People with an Internet connection that may get slow at some point (train entering a tunnel as an example).
SSR sometimes comes with unexpected results if your frontend algorithms rely on DOM manipulations so be prepared to mock some results.
If your backend uses Node.js, you’re all set. If it doesn’t, it is still possible, although it may sometimes be tricky.
If you load a lot of images on the page, loading of your script may be postponed. Think about lazy loading of images so that your script may start downloading faster. Alternatively, you may insert low-quality versions of the same images in a similar way to Medium.
Maybe the “script” tag with your bundle is too low in the DOM? Moving it as high as necessary may sometimes be crucial.
Modern browsers support all ES6 specifications (maybe except for modularity, which can be easily covered anyway). We may create two build configurations for ES5 and ES6 builds respectively. The ES6 build may be shipped to selected browsers and ES5 to all the rest. Your backend server may send the first or second bundle based on the user agent.
As we all know, our ES6 code is babelified so that older browsers may still understand it, but it does not come without a cost. The cost of transpiling to ES5 comes with a lot of polyfills so that a simple “async await” function takes 4 times as much code after transpilation.
Yet oftentimes your node_modules take a huge majority of your bundle (and you should and will probably be forced anyway to exclude node_modules from transpilation), so in the end, it may not be worth the effort but it definitely is something to investigate in your case.
If you rely on code splitting (and often you should) and generate chunks, you may sometimes end up with too many kB chunks, which cause only blockades of download. You should specify a minimum chunk size in Webpack configuration.
If you have many chunks, you probably ended up with repeated elements in these chunks. In order to disable such behavior, you should add the following option in your Webpack configuration:
Furthermore, you may decide what should land in your chunks. All you have to do is add
`/* webpackChunkName: “chunkName” */` in your dynamic import as presented here.
“Gzip” has been supported in all browsers for a long time. Saving on bundle size as much as possible is a huge win. It’s a very easy way to reduce the size of your JS files. As mentioned here it can all be compromised to the following conversation:
Browser: Hey, can I GET index.js? I’ll take a compressed version if you’ve got it.
Server: Let me find the file… yep, it’s here. And you’ll take a compressed version? Awesome.
Server: Ok, I’ve found index.js (200 OK), am zipping it and sending it over.
Browser: Great! It’s only 10kB. I’ll unzip it and show the user.
To make sure your server sends “gzip” files, check for “content-encoding” header in the files your client downloaded.
Yet sometimes we exaggerate with the number of libraries.
Install Webpack Bundle Analyzer and track the heaviest parts of your app. To give you an example, Moment.js weighs 65kB minified and gzipped and 75% of its weight is caused by the huge number of locales it provides. Consider removing some (or most) locales from your build or use a lightweight alternative such as “date-fns” or “dayjs”.
Remember that if a user sees your content fast, he will likely stay on your site. At the end of the day, it is always good to go through a tool such as Lighthouse to find other problems that your web app may suffer from.