Optimize app startup

Generate app startup statistics

Any app managed with the map.apps Manager can be configured to collect app startup statistics.

To generate statistics for an app, follow these steps:

  1. Open the app in the map.apps Manager App Editor

  2. Add the property "enableStatistics": true to the properties section of the app.json file.

    To enable collection statistics for all apps, set client.config.statistics.enabled=true in the application.properties. In that case, you don’t need to set enableStatistics property in app.json file of an app.
  3. Add the bundle apprt-statistics to the app.

    enable statistics

  4. Load the app in the same browser window as the Manager.

    Now, when you start the app, two buttons and some startup statistics are displayed in the top-left corner of the browser.

    enable statistics buttons

  5. Click Save Stats to save the detailed CSV in a file. You need to be logged in to the map.apps Manager, otherwise you will get an error. The CSV file is saved in the database, where all other app data is stored.

    Click Print Stats to open a text input with detailed CSV structured data of the collected data.
  6. To access the CSV file, close the App Editor of the app and reopen it.

  7. In the App Editor open the file chooser and select the saved data file.

To interpret the CSV structure you need deeper knowledge about map.apps. Very important are the rows BundleController#_startBundle, which list the time a bundle requires to be activated. This helps to find hot spots.

For a first impression the green text shows the time spent until the internal event FRAMEWORK_STARTED is fired. It is separated into a boot and bundles section. The boot section is the time required to prefetch all JavaScript data (if enabled) and to initialize the system bundle. The bundles section displays the time required to activate all bundles. This time does not cover the time until the map is loaded.

Important configuration options

client.config.preFetchBundles

This activates pre-fetching of JavaScript files. This reduces the amount of requests made by the browser by fetching a lot of JavaScript files with a single server request. These generated files are called layer files.

To check if this flag is enabled assert that two resources/jsregistry/root/layer.js?mids=…​. requests are executed. If a lot of JavaScript files are loaded separately — for example <bundlename>/module.js files — this is an indicator for a broken setup.

Default: true

client.config.defaultExpiresHeaderInDays

Adds Expires HTTP headers to the files delivered by map.apps. This means that a browser is allowed to cache the files fetched from map.apps. The default value of 0.5 makes the browser caching the files for half a day.

A value of 0 defines that a browser does not cache any files.

Default: 0.5

jsregistry.cache.enabled

This enables the file cache of the JS Registry. If this is not enabled, every layer file is regenerated with every access which can take a long time. If enabled, the generated files are reused.

Default: true

jsregistry.classpathscanner.refreshperiod

Defines the interval the classpath scanner of the JS Registry detects bundle changes. Due to the fact that normally the classpath does not change it is sufficient that the scanner works only once at map.apps start.

Default: -1

jsregistry.directoryscanner.refreshperiod

Defines the interval the directory scanner of the JS Registry detects bundles. Set to -1 in production environments. In production environments bundles in the file system do not change. In that case it is sufficient scan only on map.apps startup.

Default: -1

Experimental: Manipulate the initialization of base maps and layers

By default all layers are pre-loaded and initialized independent of their visibility state. To manipulate this behavior, see the map-init bundle documentation .

Experimental: lazy startup

To define that some bundles are started after all other bundles or after the event FRAMEWORK STARTED, add a lazy attribute to the load section of the app.json file.

"load": {
    "allowedBundles": [
           "system",
           "splashscreen",
           "map",
           "..",
           "measurement",
           "editing",
           "templates",
           "templateslayout",
           "templateslayout-dijit"
    ],
    "lazy" : ["measurement", "editing"]
}

Lazy loaded bundles still have to be declared in the allowedBundles list, also. The lazy flag is ignored if any other bundle requires a lazy bundle.

Experimental: Start bundles by BundleTools

Another way to reduce the initial startup time is to define that some bundles are not started initially. These bundles can be started later by clicking a dedicated tool.

This requires two steps:

  1. define the bundles that are started by default

  2. define bundle tools

To define which bundles are started directly use the start property in the load section of the app.json file.

This feature is experimental. By using the start property in an app, no optional dependencies are loaded.

To use the map.apps template mechanism, add templatelayout-dijit to the bundles that are started by default.

"load": {
    // defines that the JavaScript of all allowedBundles is initially prefetched, not only the start bundles
    "preFetchAll" :true,

    "allowedBundles": [
           "system",
           "splashscreen",
           "map",
           "..",
           "measurement",
           "editing"
    ],

    "start" : ["system","splashscreen","map","templates","templateslayout","templateslayout-dijit"]
}

To define a tool that starts other bundles, the bundle bundletools can be used. With this bundle a tool can be defined in the app.json file like this:

"bundletools": {
    "BundleActivationTool": [{
        "bundles": [
            "measurement",
            "editing",
        ],
        "id": "toggleBundles",
        "title": "Start Bundles",
        "iconClass": "icon-pencil"
    }]
}

Minimize usage of proxy

Minimize routing requests over the /proxy endpoint of map.apps. This has an impact on the performance for loading server resources, because it adds extra network latency. If the proxy is needed, configure the proxy.use.rules property to define exactly which domain needs to be routed over the proxy.

Developer tips

The startup performance highly depends on the work an Activator or a Component is performing during initialization. To optimize startup performance, reduce the runtime of initialization methods.

Activator.start()

A bundle Activator is the only class allowed to block startup by returning a Promise in Activator.start(). This feature should rarely be used but is required for some features, for example to enforce that a user is signed in or to load configuration data, which are needed by other bundles. For all other situations, Activator.start() should return as quickly as possible and never return a Promise.

Component.activate()

The App Runtime parses all component declarations during the start of a bundle. If all requirements of a component are fulfilled at parsing time, the component is instantiated, which has a direct impact on startup time. Like Activator.start(), activate() and init() should finish as fast as possible. A good pattern is to initialize the internal state of a component asynchronously so that activate() is not blocked.

Reference Injection

A developer should be careful with the work performed in injection methods like add<reference name>(), remove<reference name>(), set<reference name>(), unset<reference name>(). These methods are called by the App Runtime during the start of a component to inject the declared references. These methods should also be fast. It is bad practice if injection changes trigger widget or DOM changes directly. A good pattern is to react asynchronously inside these methods, for example to queue the parameters and perform the changes some milliseconds later.

MapModel events

The MapModel events onNodeStateChanged() and onStructureChanged() have a very high impact on the mapping performance. It becomes especially critical if the layer tree is very complex. Widgets which rebuild their UI based on these events should react asynchronously to these events.

A complex app should try to minimize the initial MapModel complexity. The startup time of the map bundle can be optimized by using deferred registration of initially invisible operational layers, for example.

When modifying the MapModel programmatically a developer must use the correct event to notify about changes. If nodes are added or removed from the model fireModelStructureChanged() has to be fired. This causes the re-initialization of the map and related components. In case only properties of nodes are changed, for example enabled, the fireModelNodeStateChanged() has to be fired. The latter only changes widgets, which is way more efficient than rebuilding all related widgets.