Some technical review from impressions.

You’re loading about 430KB of JavaScript. That’s quite a lot to parse and execute (yeah, less than many sites, but judging by the functionality it could probably be under 20KB for people just reading the site—code splitting, and all that). Gzipped, it’s over 70KB. Feeding it through Terser to minify the code (which achieves much more than just joining the lines), you can trivially cut it down to about 370KB with no breakage, and treating it as a module (so that it can mangle top-level names) reduces it to 230KB/50KB, which would execute distinctly faster, except for the minor matter that everything will break because event handlers are routinely expressed as strings, including function names (and that’s probably the biggest reason why you should separate event handlers from markup).

You’re reimplementing various things that the browser provides, badly. A few examples:

• <a href='javascript:…'>. This is always bad. Either use a link with a meaningful non-JavaScript href that will work with things like Ctrl+click/open in new tab and possibly add a click handler that will event.preventDefault() and do something different for the current tab (though for general navigation in SPAs it’s normally easier to attach just one “in-app link” interceptor on the document root), or use a button with a click handler.

• <div onclick>/<span onclick>. This is almost always bad (use an element with click semantics, such as a link or a button), and always bad if the element is not focusable by keyboard (e.g. by the tabindex="0" attribute). When it’s <span onclick="javascript:appSwitchPage('/conversation/xjbjpgpqrpvutlf8wcgz1gt50')">, well, that’s even worse than <a href="javascript:appSwitchPage(…)">. Your approach there should be closer to <a href="/conversation/xjbjpgpqrpvutlf8wcgz1gt50" onclick="event.preventDefault();appSwitchPage(this.href)">, though again I say one root link interceptor is generally a better approach.

• History management: you’ve implemented your own back button, a strange choice in general (why not use the browser’s like everyone else? For some sorts of apps, an up button that takes you to the parent in a hierarchy can make sense, but I can’t immediately think of any reasonable case for a back button), but your history management also just interacts badly with the browser’s own, with your back button adding a new history entry, and clobbering history when the native back button is used.

• Page loading: every time you load a page, you use your own loading spinner, and don’t cache various things that reasonably could be. This is a massive regression in experience from the traditional MPA/server-generated HTML approach, where you tend to stay where you are, with only a small indicator that things are happening, until it’s ready and changes over. The end result of all of this is that this site feels much slower to load than it should, and that every load is painful and very disrupting. I’m in Australia, by the way, so add about 150ms on to whatever you may be used to—though the responses seem to be slow enough that latency doesn’t dominate it.

Other issues:

• Relative time display looks to be anchored to when you loaded the page/app, not when you loaded a particular view/page. This leads to times being wildly wrong, including future times (“-1234 seconds ago”). The problem looks to be the way settings_timestamp is used.

• The style of the code leads me to be concerned about injection attacks; in most places, the client is mixing template markup and responses from the server as raw HTML, thereby trusting the server to protect against malicious markup. I tried creating an account with values like <script>alert("xss")</script> for name/alias/bio/whatever, name netted me a crash on /settings/me when I click on the value which shows <script>alert("Name")</script&, “SyntaxError: missing ) after argument list”. I’ve gone no further.