Files
status-react/src/utils/transforms.cljs
T
Icaro Motta c1d2d44da4 perf: Fix app freeze after login (#20729)
We do a few things to reduce the initial load and make the app more responsive
after login. The scenario we are covering is a user who joined communities with
a large number of members and/or which contain token-gated channels with many
members.

- Related to https://github.com/status-im/status-mobile/issues/20283
- Related to https://github.com/status-im/status-mobile/issues/20285

- Optimize how we convert a community from JS to CLJS. Community members and
  chat members are no longer transformed to CLJS, they are kept as JS. Read more
  details below.
- Delay processing lower-priority events by creating a third login phase. The
  goal is to not put on the same queue we process communities less important
  events, like fetching the count of unread notifications. Around 15 events
  could be delayed without causing trouble (and this further prevent a big chain
  of more events to be dispatched right after login).
- Tried to use re-frame's flush-dom metadata, but removed due to uncertainty,
  check out the discussion:
  https://github.com/status-im/status-mobile/pull/20729#discussion_r1683047969
  Use re-frame’s support for the flush-dom metadata whenever a signal arrives.
  According to the official documentation, this should tell re-frame to only
  process the event after the UI has been updated. It’s hard to say if this
  makes any difference, but the theory is sound.
- Reduce the amount of data returned to the subscription that renders a list of
  communities. We were returning too much, like all members, chats, token
  permissions, etc.

Other things I fixed or improved along the way:

- Because members are now stored as JS, I took the opportunity to fix how
  members are sorted when they are listed.
- Removed a few unused subs.
- Configured oops to not throw during development (in production the behavior is
  to never throw). This means oops is now safe to be used instead of interop
  that can mysteriously fail in advanced compilation.
- Show compressed key instead of public key in member list for the account
  currently logged in.

Technical details

The number one reason affecting the freeze after login was coming from
converting thousands of members inside communities and also because we were
doing it in an inefficient way using clojure.walk/stringify-keys. We shouldn't
also transform that much data on the client as the parent issue created by
flexsurfer correctly recommends. Ever since PR
https://github.com/status-im/status-mobile/pull/20414 was merged, status-go
doesn't return members in open channels, which greatly helps, for example, to
load the Status community. The problem still exists for communities with
token-gated channels with many members.

The current code in develop does something quite inefficient: it fetches the
communities, then transforms them recursively with js->clj and keywordizes keys,
then transforms again all the potentially thousands of member IDs back to
strings. This PR changes this. We now shallowly convert a community and ignore
members because they can grow too fast. From artificial benchmarks simulating
many members in token-gated channels, or communities with thousands of members,
the improvement is noticeable.

You will only really notice improvements if you have spectated or joined a
community with 1000+ members and/or a community with many token-gated channels,
each containing perhaps hundreds of members.

What's the ideal solution?

We should consider removing community members and channel members from the
community entity returned by status-go entirely. The members should be a
separate resource and paginated so that the client doesn't need to worry
about the number of members, for the most part.
2024-07-25 21:23:08 -03:00

99 lines
3.3 KiB
Clojure

(ns utils.transforms
(:refer-clojure :exclude [js->clj])
(:require
[camel-snake-kebab.core :as csk]
[cljs-bean.core :as clj-bean]
[oops.core :as oops]
[reagent.impl.template :as reagent.template]
[reagent.impl.util :as reagent.util]))
(defn js->clj [data] (cljs.core/js->clj data :keywordize-keys true))
(defn clj->pretty-json
[data spaces]
(.stringify js/JSON (clj-bean/->js data) nil spaces))
(defn clj->json [data] (clj->pretty-json data 0))
(defn <-js-map
"Shallowly transforms JS Object keys/values with `key-fn`/`val-fn`.
Returns nil if `m` is not an instance of `js/Object`.
Implementation taken from `js->clj`, but with the ability to customize how
keys and/or values are transformed in one loop.
This function is useful when you don't want to recursively apply the same
transformation to keys/values. For example, many maps in the app-db are
indexed by ID, like `community.members`. If we convert the entire community
with (js->clj m :keywordize-keys true), then IDs will be converted to
keywords, but we want them as strings. Instead of transforming to keywords and
then transforming back to strings, it's better to not transform them at all.
"
([^js m]
(<-js-map m nil))
([^js m {:keys [key-fn val-fn]}]
(when (identical? (type m) js/Object)
(persistent!
(reduce (fn [r k]
(let [v (oops/oget+ m k)
new-key (if key-fn (key-fn k v) k)
new-val (if val-fn (val-fn k v) v)]
(assoc! r new-key new-val)))
(transient {})
(js-keys m))))))
(defn js-stringify
[js-object spaces]
(.stringify js/JSON js-object nil spaces))
(defn js-parse
[data]
(.parse js/JSON data))
(defn js-dissoc
[js-object & ks]
(let [object-copy (.assign js/Object #js {} js-object)]
(doseq [js-key ks]
(js-delete object-copy (name js-key)))
object-copy))
(defn json->clj
[json]
(when-not (= json "undefined")
(try (js->clj (.parse js/JSON json))
(catch js/Error _ (when (string? json) json)))))
(def ->kebab-case-keyword (memoize csk/->kebab-case-keyword))
(def ->PascalCaseKeyword (memoize csk/->PascalCaseKeyword))
(defn json->js
[json]
(when-not (= json "undefined")
(try (.parse js/JSON json) (catch js/Error _ (when (string? json) json)))))
(declare styles-with-vectors)
(defn ^:private convert-keys-and-values
"Takes a JS Object a key and a value.
Transforms the key from a Clojure style prop to a JS style prop, using the reagent cache.
Performs a mutual recursion transformation on the value using `styles-with-vectors`.
Based on `reagent.impl.template/kv-conv`."
[obj k v]
(doto obj
(oops/gobj-set (reagent.template/cached-prop-name k) (styles-with-vectors v))))
(defn styles-with-vectors
"Takes a Clojure style map or a Clojure vector of style maps and returns a JS Object
valid to use as React Native styles.
The transformation is done by performing mutual recursive calls with `convert-keys-and-values`.
Based on `reagent.impl.template/convert-prop-value`."
[x]
(cond (reagent.util/js-val? x) x
(reagent.util/named? x) (name x)
(map? x) (reduce-kv convert-keys-and-values #js {} x)
(vector? x) (to-array (mapv styles-with-vectors x))
:else (clj->js x)))