researchstudio-sat / webofneeds

Finding people to cooperate with. Protocol, not platform. Decentralized. Linked Data. Open Source.
http://researchstudio-sat.github.io/webofneeds/
Apache License 2.0
63 stars 20 forks source link

Page-load performance optimisation #546

Closed pheara closed 7 years ago

pheara commented 8 years ago

Here's a few enhancements we could make to drastically increase the page-load duration, roughly sorted by how much effort they are to implement:

  1. Minify the bundle (expecting 50%-70% saved in size)
  2. Make sure server gzips the bundle
  3. Make sure the big bundle is cached by the browser (saves time on reloads)
  4. Everything loaded by the load-action as pre-baked script exporting an initialisation object, included directly in index.jsp. This saves us a server round-trip.
  5. Find out which scripts are bloating the bundle's size and replace them with slimmer alternatives.
  6. divide the bundle into chunks.first load scripts necessary for the actively displayed view, then load rest in a second pass while the user is still on that initial view.
fkleedorfer commented 8 years ago

This is really a showstopper for many people.

pheara commented 7 years ago

An first step that we should take before starting with service-workers, would be to generate a hash of every built bundle.js and append that to it's name and statically generate index.html with <script src="bundle-a7ad875aef.js"></script>. Then we can set the caching-policy with which it's served to "forever".

This would also have the nice benefit of fixing caching issues when debugging.

pheara commented 7 years ago

We could add the clojure-compiler to the build. Among other things it does limited tree shaking and minification. For a different project I'm currently working on, this reduced the app-bundle size from 2.2MB to 550kB (140kB after gzipping). Dunno if we can't also get babel/jspm to minify/tree-shake, though.

pheara commented 7 years ago

Also, we should go over our javascript dependencies, check their file-size and try to replace the larger ones with slimmer alternatives.

pheara commented 7 years ago

Related issues:

pheara commented 7 years ago

As for minification: there's a jspm-option for that. Just add minify: true to the call to gulp_jspm(..) in gulpfile.js. This will cause some compilation problems though, that need to be fixed (e.g. missing semicolons).

pheara commented 7 years ago

jspm allows us to split bundle. If angular allows adding components later at run-time, we could pull the landing page it's dependencies into a seperate bundle and load the rest of the app in a second pass. Dunno how we'd react to people entering via the create-need or post-detail pages? have an initial-deferred pair for every possible entry-point?

fkleedorfer commented 7 years ago

The worst part is loading all the data. Possible approaches:

  1. dropping the in-browser rdf store and making the owner webapp server side fatter/smarter
  2. Figuring out what ingormation is needed per need and which needs are needed, and load only that
pheara commented 7 years ago

Some additional enhancement-possibilities that came up during today's jour fixe:

pheara commented 7 years ago

screenshots of the state of things at 69de16c, loading the app with throttling set to "fast 3g" (1Mb/s down, 750kb/s up, 40ms RTT):

on the landingpage without an account (12.5s without, 16s with fonts):

loading_landingpage_-_good_3g

note that the map loads tiles towards the end for about 4s, even though it's not visible by default. performance wise this is actually good, should the user continue with a view with a visible map, but has the downside of occuring additional data-transmission (which should be avoided due to causing additional charges)

roughly 12.5s (excluding fonts) till the app has finished loading, 16s with them, most of which is spent loading data over the network (as there's almost no gaps between the requests)

landinpage, w/o account, gzip enabled (8.5s w/o fonts, 14s with them): (ADDED LATER)

loading_landingpage_-_good_3g_-_with_gzip

As you can see, page-load has improved from 12.5s to about 8.5s (without fonts; 16s to 14s with them), thus yielding roughly a 4s improvement (32% of load-time) to first meaningful render (i.e. till text and buttons of the landing page are visible, even if just with a backup-font)

on the feed, signed in, with 5 owned needs (18.5s):

loading_feed_with_5_posts_-_good_3g

loading the needs and connections incurs an additional 6.5s (onto a slightly lower base of 12s) for a total of around 18.5s in this case. Most of that time is spend on the round-trips (the actual data-transfer has neglible impact). Note, that the requests for the needs and connections are somewhat cascaded, as first the owned needs, then their connections and then their counterparts are fetched.

Roughly 2s of the app-bundle's load time are spent waiting for a free tcp socket, as it's listed last. By preloading it via <link prefetch...> (for a usage-example see number seven here) we could (probably) shave those off.

Note:

For a legend and explanation of the color-coding, see here

pheara commented 7 years ago

also rather interesting,

on the feed, signed in, with 5 owned needs, repeated visit (22s):

loading_feed_with_5_posts_-_good_3g_-_repeated_visit

The sprite and the <script>-included dependencies are cached now. For some reason however, it redownloads them -- sequentially -- thus causing the even higher load time.

pheara commented 7 years ago

To summarize possible approaches, for general page-load performance we have:

  1. {x} #1201(pr): serve the minified css (we already generate it)
  2. {x} #1203(pr): make sure bundle starts loading asap (e.g. via <link prefetch...>, see number 7 here). Update: link-preload/prefetch had no effect, however it was possible to bundle mediumjs/rangy/undojs and thus have the bundle load in the first round of ressources after the html is parsed.
  3. {x} #1198: configure server to use gzip (expected size-reduction of bundle of 1M/ by 73%)
  4. {x} #1199(pr): use gzip-compression for fonts
  5. {x} #1205(pr): remove unused font-weights
  6. {x} #1205(pr): non-blocking font-loading / using fall-back fonts
  7. {x} #1205(pr): woff/woff2 fonts (~10% of the size)
  8. {x} #1210(pr): gzip svgs
  9. {x} #1203(pr): inline* or bundle <script>-includes
  10. {x} add tree-shaking (e.g. via the clojure compiler or by figuring out how to do this via jspm). Update: see below
  11. { } bundle/prebake getDefaultWonNodeUri; e.g. via config.js or a jsp-template. Ideally this should be taken from the owner-config in conf.local.
  12. { } remove whitespace-characters from template-strings during minification (atm the minifier doesn't do that). This gulp-plugin might be helpful.
  13. { } find out how to make more of these files inline-able and thus tree-shakable. (Intented files are already inlined).
  14. { } HTTP/2-serverpush on owner-server to start pushing required/mandatory ressources without additional roundtrips. This stack-overflow-thread mentions support from tomcat 8.5 onward (we're currently on 8.0) and this comment talks about a PushBuilder API. It might be necessary to code this out ourselves as I couldn't find a straightforward configuration example for tomcat.
  15. { } make node return need, connections and counterparts a single round-trip (if possible)
  16. { } split bundle, per view into things needed now and needed later
  17. { } look into ways of removing lodash (indirect dependency via npm:redux, see below)
  18. { } look for slimmer alternatives for some of our dependencies (or split off the parts we need from them)
  19. { } #1226: remove color variations from sprite-sheet and use css instead (~50% size reduction ~> 0.1s)
  20. { } inline* styles (?)
  21. { } inline* fonts (?) (via inlined css)
  22. { } inline* bundle (?)
  23. { } Find out why OpenSans-Regular is loaded from local font folder, whereas -Bold and -Light aren't. => load from disk for the few people with the font installed.
  24. { } enable br(otli)-compression if client supports it (it compresses better than gzip; might not be possible in tomcat atm, but it is in nginx via plugins)
  25. { } replace angular with smaller rendering layer (see below)
  26. { } prerender, serve static html and inflate on client (reduces time to meaningful render, not time to interactive; afair not possible in angular)

ad inlining: as David Gilbertson points out in number six here, index.html should stay below 14kB to avoid running into flow-control. We could still try it though, as we spent roughly 0.5s per request on TTFB and our first html isn't too meaningful anyway before the javascript has finished initializing everything (unless of course we go down the path of server-side prerendering).

For repeated visits we could do the following:

  1. {x} #1207(pr): gzip on node-server (speeds up loading of needs/connections/events)
  2. { } figure out why chrome redownloads the css, fonts and svg (as described here)
  3. {x} ensure bundle can be cached / chache it via webworkers
  4. { } cache needs/connections/events on owner-server and add them to index.hmtl when serving it
  5. { } cache needs/connections/events locally in the idb via webworkers (or in the app)
  6. { } selective loading of needs for >5 needs and >10 connections
  7. { } fine-tuning long-time caching, revving, etc (see here)

First, I'd like to diagnose how much size the dependencies in our package json and our app-folder are contributing to the bundle though.

pheara commented 7 years ago

For when we go super-perfomance, https://hpbn.co/is a good ressource to read. Though, at this point of time, most of it is rather neglectable for us, compared to the bundle-size and the time required to fetch needs and everythin attached to them.

pheara commented 7 years ago

For analyzing our dependencies contribution to bundle-size there's this nice treeview-app (it would require setting up an alternative, webpack-based build though)

I've listed the sorted the sizes of all javascript-files in ./jspm_packages/* here. However this doesn't tell us which versions (for packages containing multiple variants), how large they are minified (unless they already are) and to what dependencies of ours they contribute to (e.g. we don't use lodash, one of the worse offenders, directly, but it's included via npm:redux), as such we don't know how much each direct dependency ultimatively adds to the bundle atm.

heikofriedrich commented 7 years ago

hier sind die top punkte komprimierung von js, aufspalten und parallel laden von js und js reduzieren: https://developers.google.com/speed/pagespeed/insights/?url=www.matchat.org wobei man sich warscheinlich vor der komprimierung gedanken über aufspalten, reduzieren und paralelisieren machen muss, komprimieren kann man dann immer

pheara commented 7 years ago

One of our larger dependencies and the only dependency that's larger than our app is angular, that we mostly use as a rendering layer. In this capacity we could replace it with just about anything else and save significantly in size (e.g. react, preact, riot, etc). For switching to react (the largest of the alternatives), this would reduce our bundle-size by about 10% and thus have roughly the same effect as reducing our the size of the scripts in our app/-folder by about 40% (assuming the minified sizes listed below). However, it would require quite some work -- mostly refactoring the component's boilerplate code and switching out the router -- and thus shouldn't be the highest priority in my opinion.

   8276 preact.min.js
  20772 preact.js
  23040 react.min.js
 145032 react.js
 168828 angular.min.js
 380532 app/ (rough minification estimate*)
 962560 app/ (unminified)
1270864 angular.js
1473863 app_jspm.bundle.js (minified)
5016465 app_jspm.bundle.js (not minified)

* using the more realistic factor in this quick and rough estimation calculation

One of the other big-offenders, that we really don't need is lodash, that we get indirectly via npm:redux and npm:ng-redux that depends npm:redux and several lodash-plugins. We should look into alternatives or write our own. Per se the redux dispatcher should be rather straightfoward/small and possible to implement without lodash (but i'm probably mistaken in the first regard). Maybe there's also a lodash-free variant or alternative. In any way, we'd also need to replace the utilities gained from ng-redux. Doing this successfully, would reduce our bundle-size by about 9%.

   5782 jspm_packages/npm/redux@3.7.2/dist/redux.min.js
  26736 jspm_packages/npm/redux@3.7.2/dist/redux.js
 134829 jspm_packages/npm/lodash@4.17.4/lodash.min.js
 213073 jspm_packages/npm/lodash@4.17.4/lodash.js
pheara commented 7 years ago

@heikofriedrich ah, this answers the question whether or not our server uses gzip -- apparently it doesn't! That should be a rather low hanging fruit indeed we've got there :)

heikofriedrich commented 7 years ago

yeah, this google develeoper performance site looks quite useful to me, maybe we could use this to asses how successful our performance optimizations were in the end => current measurement: 30/100 (mobile), 37/100 (desktop)

pheara commented 7 years ago

As for tree-shaking: this page of the jspm-0.17-beta documentation mentions tree-shaking. I've openened a branch (feat__treeshaking-with-bower, named to avoid clashing with a failed branch off of refac__removed-bower). In that branch I added jspm-0.17 to the repo and in the powershell (necessary to see the color-highlighting in the output) bundled the app via

 jspm build app/app_jspm.js generated/build.js --minify

This was done using the CLI, as gulp-jspm doesn't support jspm-0.17 yet and thus our bundlejs-task fails.

Tree-shaking works, as a function I added to redux-utils.js -- one of the files inlined by rollup (as is indicated by the intendation and color-highlighting) and thus one of the files benefitting from tree-shaking. The function wasn't contained in the output.

However, the resulting build.js is bigger than the app_jspm.bundle.js build on master. It appears, that we're already using tree-shaking (the same test works as well in redux-utils.js on master) and for some reason the jspm-0.17-beta output is about 1M bigger, as can be seen here:

-rw-r--r-- 1 ksinger 1049089 1475004 Sep 12 14:53 app_jspm.bundle.js
-rw-r--r-- 1 ksinger 1049089 6425584 Sep 12 14:53 app_jspm.bundle.js.map
-rw-r--r-- 1 ksinger 1049089 2512155 Sep 12 17:14 build.js
-rw-r--r-- 1 ksinger 1049089 3161453 Sep 12 17:14 build.js.map

Update: I'm not sure anymore that we're using tree-shaking with the old jspm version -- I can't repeat the experiment (having a function removed) anymore, unless I build with the newer jspm-version in the changed build-setup/config environment of eat__treeshaking-with-bower. What I do know however, is that the output generated by jspm-0.17 is bigger than with our current setup, so I'd advise against changing right now.

pheara commented 7 years ago

For measuring the actual size-impacts of our dependencies (including their dependencies), we could simply change jspm_config.js to point to an empty module export for each of these in turn, build the bundle and check the size differences. This should even be scriptable without too much effort.

pheara commented 7 years ago

landinpage, w/o account, after enhancements (4.5s w/o fonts, 5.5s with fonts):

loading_landingpage_-_good_3g_-_a-few-enhancments

With the following enhancements merged:

  1. {x} #1201(pr): serve the minified css (we already generate it)
  2. {x} #1203(pr): make sure bundle starts loading asap (e.g. via <link prefetch...>, see number 7 here). Update: link-preload/prefetch had no effect, however it was possible to bundle mediumjs/rangy/undojs and thus have the bundle load in the first round of ressources after the html is parsed.
  3. {x} #1198: configure server to use gzip (expected size-reduction of bundle of 1M/ by 73%)
  4. {x} #1199(pr): use gzip-compression for fonts
  5. {x} #1205(pr): remove unused font-weights
  6. {x} #1205(pr): non-blocking font-loading / using fall-back fonts
  7. {x} #1205(pr): woff/woff2 fonts (~10% of the size)
  8. {x} #1210(pr): gzip svgs
  9. {x} #1203(pr): inline* or bundle <script>-includes

We're now down to 4.5s (w/o fonts) and 5.5s (with fonts) on "fast 3G"

pheara commented 7 years ago

As for in-browser-caching: Chrome seems to have troubles caching won.min.css and occassionally OpenSans-*.woff2 and icon-sprite.svg. The only difference to e.g. the request for app_jspm.bundle.js is that these requests don't contain cache-control: no-cache and the responses to them contain expires: <now+1day> (while the response with the bundle doesn't). However I don't see how to add this field in the <link...>-tag, i.e. afaict we could only add this by loading the ressources via js or by intercepting the requests and adding the fields.

Other ways to fine-tune the caching:

quasarchimaere commented 7 years ago

can we close this item for the time being as we did plenty of performance enhancement as is, and we can open new specific items whenever we feel like it anyway