Closed nolanlawson closed 1 week ago
Wonder if this was webpack 1 or 2.
@TheLarkInn Webpack 1
@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.
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. 🙂
Is #7391 related?
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.
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
Now that we've moved things around the code splitting works a lot better
The largest bundle is the rust crypto wasm blob which we have no control over
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. :smiley:
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) andbundle.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 astats.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:
The largest dependency by far is
matrix-react-sdk
:matrix-react-sdk
contains a lot of code, but the biggest offender ishighlight.js
. It seems we are pulling in all ofhighlight.js
including all plugins for all languages.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 wherehighlight.js
is included (HtmlUtils.js
):This file also contains a reference to
emojione
, which is another 4.2% of the bundle size:Using
require.ensure()
in this file to trigger-load justemojione
(4.2%) andhighlight-js
(12%) would already trim 16.2% from the bundle size.Also inside of
matrix-react-sdk
is a very largelib
folder which contains components, views, dialogs, etc.: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:
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.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:
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 Nodecrypto
); maybe WebCrypto could be leveraged here?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 modevelocity.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 bydraft-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 anes6-promise
polyfill anyway via the above-mentionedcore-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 usebtoa()
instead.Core app code itself
The core code for
vector-web
, inside of thelib/
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
andhighlight.js
) should ideally be split out usingrequire.ensure()
(or the non-Webpack equivalent in case you plan on switching from Webpack).I'd advise breaking
HtmlUtils.js
out frommatrix-react-sdk
into a separate module so that it can be easily trigger-loaded fromvector-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-inBuffer
in the Node version of yourVectorConferenceHandler.js
file andBlob
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 inmatrix-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. :smiley: