Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bundle.js is way too big and should progressively load to speed up initial launch #2498

Open
nolanlawson opened this issue Oct 23, 2016 · 6 comments

Comments

@nolanlawson
Copy link

nolanlawson commented Oct 23, 2016

Based on a tweetstorm from Alex Russell about the perf of Riot, I thought I'd look into your JS bundle and provide some guidance. Unfortunately I don't have any PRs to contribute because it's a lot of code and I think you may need some significant refactoring to remove/defer the bigger dependencies, but there are also some small wins that seem easy enough to make.

Also, because y'all have been kind enough to make the code open-source, I figure that doing a public perf analysis could be instructive. 😃

High-level view

As Alex notes, there's far too much JavaScript on first load. Building the production version of Riot, I end up with two bundles: olm.js which is 438kB (128kB gzipped) and bundle.js which is 2.22MB (621kB gzipped).

The total bundle size (almost 3MB) is extremely large (although not the largest I've seen in a modern webapp, sad to say). Our first goal should be to isolate large dependencies so that we can trigger-load them using Webpack code splitting. Barring that, large dependencies should be removed entirely where applicable.

olm.js appears to be a cryptographic library which probably isn't needed on first load of the page. Seems like a good candidate to defer until later.

bundle.js is more complex, so let's analyze it using Webpack Visualizer. You'll need a stats.json file which you then upload to the Visualizer; you can generate it using stats-webpack-plugin (see branch).

The visualizer reports ungzipped/unminified sizes but it's a pretty good yardstick. Let's dive in.

bundle.js analysis

95% of the bundle size comes from dependencies:

screenshot 2016-10-22 17 17 51

The largest dependency by far is matrix-react-sdk:

screenshot 2016-10-22 17 19 39

matrix-react-sdk contains a lot of code, but the biggest offender is highlight.js. It seems we are pulling in all of highlight.js including all plugins for all languages.

screenshot 2016-10-22 17 59 39

The syntax highlighting for an obscure language called AutoIt takes up 1.9% of the total bundle size (!), Mathematica is 1.2%, SQF is 0.59%, and so on. I imagine most of these are languages that users will never need syntax highlighting for, so it's a shame that they're included on first load.

Looking inside of matrix-react-sdk, we can see where highlight.js is included (HtmlUtils.js):

screenshot 2016-10-22 17 23 01

This file also contains a reference to emojione, which is another 4.2% of the bundle size:

screenshot 2016-10-22 17 23 48

Using require.ensure() in this file to trigger-load just emojione (4.2%) and highlight-js (12%) would already trim 16.2% from the bundle size.

Also inside of matrix-react-sdk is a very large lib folder which contains components, views, dialogs, etc.:

screenshot 2016-10-22 17 26 36

This seems to be a case of lots of little files adding up. Unfortunately there's no easy fix, but here are a few strategies you could try:

  1. Use Rollup to bundle all the tiny files into one big file, which in my experience should save around 5% of the total size (assuming no dead code, which Rollup would further eliminate).
  2. Trigger-load all of matrix-react-sdk. Unfortunately it seems to be used in a lot of places, so I don't know how practical that is, but since it's about 1/3rd of the total bundle size, this is potentially a huge win.
  3. Better yet, trigger-load all the views only when those views are shown. This is more work than the Rollup solution or the "trigger-load the whole SDK solution", but would result in the biggest wins since each view would only require the code it needed.

React and other deps

React takes up about 11% of the bundle size. Unfortunately without server-rendering, I'm not sure how you could remove this because it seems pretty integral to the app:

screenshot 2016-10-22 17 29 57

matrix-js-sdk takes up another 11% of the size; seems to contain a lot of crypto logic that could possibly be deferred. This also seems to be including its own crypto library (different from core Node crypto); maybe WebCrypto could be leveraged here?

screenshot 2016-10-22 17 38 18

Other deps that may be worth cutting/deferring:

  • draft-js – 5.1% of total bundle size, could be trigger-loaded until we're in edit mode
  • velocity.js – 4.5% – maybe use CSS animations or vanilla FLIP animations instead?
  • core-js – 3.6% – you might not be using all of these polyfills; maybe consider refactoring so that each ES6 dependency is a separate dependency so you can manage them separately? e.g. Promise/Symbol/Array.includes/etc. can all be used via separate polyfills instead of needing to include one big Babel polyfill. Unfortunately this would require grepping your codebase to see where you may be using ES6 features. Even more easily, you could just cut support for older browsers.
  • immutable – 2.5% – this library provides a lot of convenience and some runtime perf wins for React, but it's a lot of code.
  • lodash – 2.2% – consider lodash-webpack-plugin or e.g. require('lodash/methodName') to avoid pulling in all of Lodash. Unfortunately lodash is a transitive dependency of a lot of other dependencies, so it's hard to tell which one is pulling in the most Lodash code.
  • fbjs – 1.7% – this is a utility library pulled in by draft-js, so another good reason to drop/defer it.
  • q – 1.1% – consider native Promises; this library seems to only be used in a couple places, and you're already including an es6-promise polyfill anyway via the above-mentioned core-js.
  • buffer – 0.87% and 0.77% – somehow two different copies seem to have been included; may I recommend Blobs and possibly blob-util instead? Also this seems to be only used in one place in your own codebase – VectorConferenceHandler.js uses it to convert to base64 when you could use btoa() instead.

Core app code itself

The core code for vector-web, inside of the lib/ folder, is 4.6% of the total bundle size. This seems to be mostly views and components, so again it could be slightly optimized by using Rollup or optimized in a more targeted way by trigger-loading each view based on the page it corresponds to.

Conclusion

I haven't had a lot of time to deeply analyze the codebase, but a few high-level observations stick out to me.

First off, the codebase seems to have been written in a style that would be familiar to Node veterans, but unfortunately Node conventions do not always work well when applied to the browser. In particular, exclusively using require() and bundling everything into one large file has reached the breaking point here, and certain large dependencies that are only used in sub-sections of the app (e.g. emojione and highlight.js) should ideally be split out using require.ensure() (or the non-Webpack equivalent in case you plan on switching from Webpack).

I'd advise breaking HtmlUtils.js out from matrix-react-sdk into a separate module so that it can be easily trigger-loaded from vector-web. In general, creating multiple small modules should increase your flexibility here. If the modularity becomes too difficult to manage as multiple repos, then I'd recommend a monorepo (e.g. using Lerna or Alle).

If you're eager to support both Node and browser versions of the code, I'd recommend leveraging the "browser" field in package.json to swap out code for the browser vs code for Node. E.g. to avoid bundling buffer you could use the built-in Buffer in the Node version of your VectorConferenceHandler.js file and Blob in another (assuming you need that particular file to work both in Node and in the browser). I see you're already using the "browser" trick in matrix-js-sdk; I would keep using it in more parts of the codebase to reduce unnecessary browser code.

Overall, there are lots of potential perf wins here, but most of them involve splitting up code and using less code. Using simple tools like the Webpack Visualizer and npm ls can help you figure out where large dependencies are getting included and how you can avoid them.

I know a lot of this is "easier said than done," and I'm sorry I don't have any code to contribute, but I hope this analysis was useful! Good luck on making Riot faster. 😃

@TheLarkInn
Copy link

Wonder if this was webpack 1 or 2.

@nolanlawson
Copy link
Author

@TheLarkInn Webpack 1

@ara4n
Copy link
Member

ara4n commented Oct 23, 2016

@nolanlawson huge thanks for the very comprehensive analysis here - the help and amount of time you've put into this really is enormously appreciated.

The reason we haven't yet got as far as doing any optimisation of the initial load time of the app is simply that we are working through major perf issues on the rest of the app first, some of which are pretty catastrophic - i.e. all of https://github.com/vector-im/vector-web/issues?utf8=✓&q=is%3Aissue%20is%3Aopen%20label%3Aperformance; the issue that @dbaron hit (some combination of #1969, #1619 or #2499); the fact that loading the initial state from Matrix can literally take minutes on big accounts (#1846) etc. Only one of these relates to the time taken to load initial JS (#126), and we've just had to prioritise progressive JS loading below all the other perf issues for now, given practically it simply isn't on the critical path of the day-to-day performance of the app.

Just to clarify: currently the time taken to display the login page of the app because of all the horrible unnecessary front-loaded JS is simply dwarfed by the amount of time it takes to (say) display the room directory, or load your chat history once you've logged in, or the cumulative time spent waiting to change rooms. As the most common use case for the app is to launch it once and then leave it running in a background tab for days or weeks, this hopefully explains the current priorities.

That said, if folks who are particularly concerned about initial app load time wanted to blitz the dependencies with async loads and incremental loading it really would be appreciated. And if nothing else, we'll get to this once the other perf fires have been put out.

@ara4n ara4n changed the title Perf analysis of JS bundle bundle.js is way too big and should progressively load to speed up initial launch Oct 23, 2016
@nolanlawson
Copy link
Author

That makes a lot of sense! I totally understand where this bloat comes from and have seen it myself on plenty of projects that were otherwise very well-architected. I agree that if most of your users are on desktop and are mostly complaining about in-app performance rather than first-load performance, then you've got the right priorities – tackle first-load after you've put out the other fires.

The hardest thing I've found about performance is that it's very difficult to "apply" after the project has already been built; usually you have to think about it from the get-go or end up needing to do huge refactors to fix creeping perf problems. If you'd like some more thoughts on this I strongly recommend Designing for Performance which is probably the best book written on the subject. 🙂

@MTRNord
Copy link
Contributor

MTRNord commented Nov 20, 2018

Is #7391 related?

@t3chguy
Copy link
Member

t3chguy commented Mar 18, 2024

Worth noting, that matrix-react-sdk is the bulk of Element Web. It isn't much of an SDK, its more than Element Web is a skin atop the Matrix React SDK project.

A lot of the original analysis is outdated (not invaluable however)

Some things are gone, some new things are in.

image

The total gzipped bundle size is now 6.6MB

It is split significantly more than it used to be but the main bundle is still 1.85MB gzipped. There's also now a 2.57MB rust crypto wasm bundle and a rust wysiwyg wasm bundle at 0.82MB gzipped which make up the majority of the overall size.

draft-js is gone
velocity.js is gone
core-js is gone
immutable is gone
fbjs is gone
q is gone

buffer and lodash are still around

I have a PR which uses React Suspense to async load some more dependencies like maplibre-gl which has taken the main bundle down to 1.38MB

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants