Web UI

Web UI Performance

Updated: August 9, 2019

In this page, we go through the different aspects of customization that may impact the performance of your Web UI apps.

Document Property Resolution

When fetching a document from the Rest API, you can request to resolve some document's extended fields. An extended field references other entities such as:

  • users or groups
  • vocabulary entries
  • other documents

in order to be able to display them. For example, in a document layout, when it comes to display who created the document (dc:creator field), if we don't ask the REST API to resolve this field, we'll only be able to display the username. With a resolved field, we can display the firstname, lastname, email, etc.

By default, Web UI requests any available field when navigating to a particular document. However, depending on your custom document types, resolving every single field may result in an overload.

Since 10.10 (NXP-26520 and NXP-25512), you can limit the list of fields that should be resolved with a contribution such as the following:

<require>org.nuxeo.web.ui.properties.contrib</require>
<extension target="org.nuxeo.runtime.ConfigurationService" point="configuration">
 <property name="org.nuxeo.web.ui.fetch.document" list="true" override="true">dc:creator</property>
 <property name="org.nuxeo.web.ui.fetch.document" >dc:nature</property>
 <property name="org.nuxeo.web.ui.fetch.document" >dc:coverage</property>
 <property name="org.nuxeo.web.ui.fetch.document" >dc:subjects</property>
</extension>

Searches

Property Resolution

Whenever a search result is populated or a Folderish document's content is listed, it queries the search endpoint. This endpoint takes into account a couple of HTTP headers.

A Web UI search is defined as follows:

<nuxeo-slot-content name="defaultSearchMenuPage" slot="DRAWER_PAGES">
  <template>
    <nuxeo-search-form name="defaultSearch" search-name="default" auto
      provider="default_search" schemas="dublincore,common,uid"></nuxeo-search-form>
  </template>
</nuxeo-slot-content>

which requests the search endpoint to serialize the dublincore,common,uid schemas of the result documents.

To make your search efficient, always limit the schemas to be serialized as much as possible. Serializing properties is costly.

The search will, by default, request to resolve all properties referencing other entities (as explained in the above section). If you have document types with a lot of references, this can significantly slow down your search because results will take much longer to be serialized by the server.

To speed up your search, you can explicitly define the properties to be resolved by specifying the X-NXfetch.document in the headers property on the nuxeo-search-form element:

<nuxeo-search-form name="defaultSearch" search-name="default" auto
  provider="default_search" schemas="dublincore,common,uid"
  headers='{"X-NXfetch.document": "dc:creator,dc:nature", "X-NXtranslate.directoryEntry": "label"}'>
</nuxeo-search-form>

In the above example, "X-NXtranslate.directoryEntry": "label" is needed to translate the label of the directory entries.

Aggregation

Aggregates are often added to a search to provide efficient search criteria and a better user experience.

However, the computation of these aggregates by the Elasticsearch back-end does not come for free.

It is not realistic to design a search that allows an end-user to trigger a complex aggregate computation on a search result set potentially containing hundreds of thousands of documents.

As a direct result, a search definition:

  • in auto mode (refresh results each time a parameter changes)
  • with complex aggregations definitions
  • potentially returning hundreds of thousands or even millions of documents

will likely not behave well because each time you change a parameter, all aggregates will be recomputed on a large amount of data.

For such use case, it is better not to use auto mode or have more specialized searches by adding different ones with a query pattern focusing on, for example, a given document type.

Since 10.3 (NXP-24880), page-provider aggregate computations can be skipped on demand to speed up the query.

Depending on your search form design, you may only wish to compute aggregates if some other parameters are set in order to restrict the result set. For example, if we'd like the Web UI default search to only compute aggregates if the fulltext parameter is not empty, we can add skipAggregates:

skipAggregates: {
  type: Boolean,
  notify: true,
  computed: '_skipAggregates(params.*)'
}

which is set to true only if the fulltext parameter is not empty:

_skipAggregates : function() {
  return !this.params || !this.params.ecm_fulltext || this.params.ecm_fulltext.length === 0;
}

HTTP Caching

Since 10.3 , we have improved caching strategy to better leverage browser HTTP cache.

Static Resources

Since 10.3 (NXP-25700), we added a Service Worker (SW) to allow for more aggressive cache on \*.html and \*.js resources by appending the server latest hot-reload or restart timestamp (TS) to their URL:

  • On clean hit, we will read resources without TS (default cache will be ineffective). SW will be installed.
  • On next hit, SW will intercept matching requests, append the TS and forward them to the network (aggressive cache will be effective)
  • On subsequent hits, SW will keep intercepting requests and network will read them from cache.

On server restart or hot-reload:

  • On first reload previous SW will still be installed and will still serve resources with previous TS. Updated SW gets installed on page load.
  • On subsequent hits updated SW will add updated TS to requests

Other static resources such as images (\*.png, \*jpeg, \*svg, etc.) have a default cache define in browser-cache-contrib.xml contribution which you can override to fit your needs.

According to the specs SWs are not active during a hard reload (e.g. Ctrl+Shift+R on most browsers).

Dynamic Resources

Since 10.3 (NXP-25385), resource URLs for document previews, thumbnails, blobs, etc. have the document's changeToken appended as a query parameter.

Such URLs have a very aggressive cache (approximately 1 year) defined in web-request-controller-contrib.xml#L47. As a matter of fact, each time the document changes, its changeToken also changes and the resource is invalidated by the browser cache.

Document Tabs

The Web UI's browser shows a set of tabs when navigating to a document. Most documents have, by default, the View, Permissions, History and Publishing tabs.

You can add new tabs (and even override existing ones) by contributing to the DOCUMENT_VIEWS_ITEMS and DOCUMENT_VIEWS_PAGES slots.

Pages introduced in the DOCUMENT_VIEWS_PAGES slot are present in the DOM even if not selected. These pages are just hidden if not displayed. If the page needs to fetch data to populate listing, then you must pay attention to the visible attribute available on your page element. The idea is to fetch the data only if your element is visible.

Here is a concrete example. We add a new tab to list the Book documents associated to an Author document:

<nuxeo-page-provider id="bookPP"
                     provider="BOOK_PAGE_PROVIDER"
                     enrichers="thumbnail"
                     params='{"book_author": [[document.uid]]};'
                     page-size="40"
                     schemas="dublincore,book">
</nuxeo-page-provider>

<nuxeo-data-table id="table" nx-provider="bookPP">
  <nuxeo-data-table-column name="[[i18n('book.title')]]" flex="100" sort-by="book:title" filter-by="book_title">
    <template>
      <nuxeo-document-thumbnail document="[[item]]"></nuxeo-document-thumbnail>
      <a class="title ellipsis" href$="[[urlFor('browse', item.path)]]">[[item.title]]</a>
    </template>
  </nuxeo-data-table-column>
</nuxeo-data-table>

Then you can see the visible property to only fetch the books when the page is displayed:

  Polymer({
    is: 'author-book-listing',
    behaviors: [Nuxeo.LayoutBehavior],
    properties: {
      document: Object,
      visible: Boolean
    },

    observers: [
      'refresh(document, visible)'
    ],

    refresh: function() {
      if (this.document && this.visible) {
        this.$.table.fetch();
      }
    }

  });

Thanks to this pattern, you can prevent useless requests from being sent and make sure your tab content is refreshed each time you display it.

We'd love to hear your thoughts!

All fields required