Performance: slow startup, clojurescript modules


#1

What has been done besides modules?

All changes above do not guarantee that index bundle file will not grow. When we add new cljs code the index becomes bigger thus startup becomes slower. Only modules can help with new cljs code.

Modules

Rationale

  • Modules allow us to load different parts of the application separately from each other, that’s a way to speed up app’s startup. Apparently code instructions are the heaviest part of js bundle for JavaScriptCore, thus moving part of js from index file is more beneficial than moving out some even “heavier” string/binary parts from it.
  • Modules help to put some discipline into the codebase by drawing some “physical” boundaries between different parts of the application and making them communicate through API

How it works

:modules {:cljs-base {:output-to "index.android.js"}
          :i18n      {:entries   #{"status_im.goog.i18n"}
                      :output-to "status-modules/cljs/i18n-raw.js"}}
  • in the code it looks like this
    (ns status-im.goog.i18n-module
      (:require-macros [status-im.modules :as modules]))
    
    (modules/defmodule i18n
      {:mk-fmt          'status-im.goog.i18n/mk-fmt
       :format-currency 'status-im.goog.i18n/format-currency})
    
    (defn mk-fmt [locale format-fn]
      ((get-symbol :mk-fmt) locale format-fn))
    
    (defn format-currency
      ([value currency-code]
       ((get-symbol :format-currency) value currency-code true))
      ([value currency-code currency-symbol?]
       ((get-symbol :format-currency) value currency-code currency-symbol?)))
    
    Using defmodule macro we specify the module’s name (in this case “status-modules/cljs/i18n.js” will be loaded when needed) and specify modules symbols which should be accessed from outside. get-symbol is defined by macro and allows to access modules symbols when necessary, on the first call it loads the whole module into the application.
  • the module can be force loaded by calling module-ns/load-module, load-module is defined by defmodule macro.
  • :cljs-base from :modules will contain all namespaces which are not specified in any of module’s :entries. We use it as an index file for RN application.
  • as far as this is rather google closures config than clojurecript compiler’s config, all namespaces should be specified in a form which can be recognized by google closure, and this means that hyphens should be replaced via underscores in the set of namespaces, e.g. "status-im.*" => "status_im.*".
  • even if some namespace is specified as a part of a module but is called directly from some namespace which is a part of :cljs-base (i.e. just required there) it will be compiled as a part of :cljs-base
  • thus all calls to the specific module should be done through its interface. If the module’s sub-namespaces are accessed directly the whole idea falls apart.
  • module’s :output-to should be specified in "status-modules/cljs/%module-name%-raw.js" format. status-modules/cljs contains all compiled module, -raw.js points that module’s file should be prepared before becoming loadable.
  • Before being used in RN app "module-raw.js" file is transformed into module.js. The whole content of "module-raw.js" is put into a string so that module.js content looks as
    module.exports = `module-raw.js content`;
    
    By doing this we make it possible to evaluate the module’s code inside the scope of the :cljs-base and thus make module’s vars accessible for the rest of the app.

How it affects existing guidelines

  • Only the core and db namespace of a module can be required outside of the module directory. Other namespaces must never be required outside of the module directory

    That definitely will not play well with modules, the module should have a single entry point which i propose to call module. All calls to core, db or anything else from inside the module should be done through module.

  • all subscriptions must be defined in the single status-im.subs namespace

    This means that we define subs which might not be used by any part of application till some specific module is loaded and doesn’t make too much sense imo. Subs might be defined inside a module if only module’s views are using it, in this case, we do not load unnecessary code on startup. Also, having a single file separated by comments contradicts a bit with the whole modularity thing.

  • all events must be defined in the single status-im.events namespace which can be considered as an index of everything going on in the app

    Same thing as with subs.

What is done already

What should be done

Other parts of application like hardwallet, wallet, profile, chat can be moved into modules.