Lazy Versus Eager Component Loading

Coveo JavaScript Search Framework 2.2900.23 (July 2017)

If you are implementing a very simple search page using only a fraction of the Coveo JavaScript Search Framework components, you will legitimately expect this page to load faster than a more complex one.

Since the July 2017 Release (v2.2900.23) of the Coveo JavaScript Search Framework, there are two different ways to load components in a search page: lazy and eager component loading.

While lazy component loading is conceptually more complex than eager component loading, using the former way in a simple implementation can result in linear performance gains during the search page loading process. These performance gains are especially noticeable on less powerful machines, such as mobile devices.

Eager Component Loading

Eager component loading sequentially loads the code of the entire framework before the initialization process of the search page, regardless of whether the page needs the totality of this code or not. Before the July 2017 Release (v2.2900.23), this was the only way to load components in the Coveo JavaScript Search Framework.

In the best case scenario (i.e., when a search page uses each and every component of the framework), this straightforward approach performs as well as lazy component loading. However, eager component loading can never perform better than lazy component loading.

To use eager component loading in your search page, you must reference the CoveoJsSearch script (not the CoveoJsSearch.Lazy script).

You can reference the script from your own server:

<head>
  [ ... ]
  <script src="js/CoveoJsSearch.js"></script>
  [ ... ]
</head>

You can also reference the script from the release of your choice (from version 1.2537 on) through the Coveo CDN (see JavaScript Search Framework CDN Links):

<head>
  [ ... ]
  <script src="https://static.cloud.coveo.com/searchui/v1.2537/js/CoveoJsSearch.min.js"></script>
  [ ... ]
</head>

Lazy Component Loading

Lazy component loading makes your search page download only the code of the components it actually requires to work.

Moreover, lazy loading downloads the code of any given component only once. Consequently, having many instances of a component (such as Facet) in a search page has no effect on its initial load time.

Being asynchronous in nature, lazy component loading adds a layer of complexity to a search page. However, it can also result in linear performance increases for simple search pages.

Suppose you implement a very basic search page such as this one:

<body id="search" class="CoveoSearchInterface" data-design="new">
  <div class="coveo-search-section">
    <div class="CoveoSearchbox"></div>
    <div class="CoveoSearchbox"></div>
  <div class="coveo-main-section">
    <div class="coveo-results-column">
      <div class="CoveoResultList">
        <script id="mySimpleResultTemplate" class="result-template" type="text/underscore">
          <div class="CoveoResultLink"></div>
        </script>
      </div>
    </div>
  </div>
</body>

Eager component loading would download the entire Coveo JavaScript Search Framework code (60+ components) before initialization, even though the search page clearly has no need for most of this code.

Lazy component loading would first scan the page for Coveo components. It would then only download the code which the SearchInterface, Searchbox, ResultList, and ResultLink components require. This could result in substantial performance improvements, especially for users with slow Internet connections, or less powerful machines, such as mobile devices.

To use lazy component loading in your search page, you must reference the CoveJsSearch.Lazy script (not the CoveoJsSearch script).

You can reference the script from your own server:

<head>
  [ ... ]
  <script src="js/CoveoJsSearch.Lazy.js"></script>
  [ ... ]
</head>

You can also reference the script from the release of your choice (from version 2.2900 on) through the Coveo CDN (see JavaScript Search Framework CDN Links):

<head>
  [ ... ]
  <script src="https://static.cloud.coveo.com/searchui/v2.2900/js/CoveoJsSearch.Lazy.min.js"></script>
  [ ... ]
</head>

Interacting with Lazy Components

The eager component loading initialization process is synchronous. This means that once the Coveo.init call is made, you can write code that interacts with your search page component instances right away, since all components are necessarily instantiated by then.

When you use lazy component loading in your search page, however, the initialization process is asynchronous. The page starts by downloading the necessary component code “chunks” as separate threads during the init call. These threads might very well  still be ongoing by the time the init call is through. Consequently, you must wait for a component promise to resolve before you can interact with an instance of this component.

Consider the following markup:

<body id="search" class="CoveoSearchInterface" data-design="new">
  <div class="coveo-search-section">
    <div class="CoveoSearchbox"></div>
    <div class="CoveoSearchbox"></div>
  <div class="coveo-main-section">
    <div class="coveo-facet-column">
      <div class="CoveoFacet" data-id="yearFacet" data-title="Year" data-field="@year"></div>
    </div>
    <div class="coveo-results-column">
      <div class="CoveoResultList">
        <script id="mySimpleResultTemplate" class="result-template" type="text/underscore">
          <div class="CoveoResultLink"></div>
        </script>
      </div>
    </div>
  </div>
</body>

Suppose you simply wish to collapse the Facet instance whose data-id is yearFacet after the DOM content of the page finishes loading.

If you are using eager component loading in your search page, you could simply do this right after the init call:

document.addEventListener('DOMContentLoaded, function() {
  [ ... ]
  Coveo.init("#search");
  // This works consistently with eager component loading.
  Coveo.get(Coveo.$$(document).find(".CoveoFacet[data-id='yearFacet']")).collapse();
  [ ... ]
)};

However, if you are using lazy component loading in your search page, doing so will likely produce an error message such as:

Uncaught TypeError: Cannot read property 'collapse' of undefined

The explanation is simple: since the lazy loading initialization process is asynchronous, the Facet component might not yet be instantiated when you call its collapse method right after the init call. Therefore, you have to wait for the component promise to resolve to make it work. The Coveo.load top-level function allows you to conveniently do so:

document.addEventListener('DOMContentLoaded, function() {
  [ ... ]
  // This works consistently with lazy component loading.
  Coveo.load("Facet").then(function(Facet) {
    Coveo.get(Coveo.$$(document).find(".CoveoFacet[data-id='yearFacet']")).collapse();
  });
  [ ... ]
)};

Registering a Lazy Component

If you want to work on a custom version of an existing component, you will likely also want to make sure the lazy component loading process recognizes it.

When you extend an existing component, you need to make sure the necessary code is available first. This means you have to wait for the “parent” component promise to resolve before you can register a “child” component implementation.

To do so, you should use the Coveo.LazyInitialization.registerLazyComponent method. This method takes two arguments: the ID of the component you want to register, and a function that returns the component implementation when its prerequisite promise resolves.

Suppose you want to register a custom Searchbox as a lazy component.

You could do so in TypeScript:

export interface ICustomSearchboxOptions extends ISearchboxOptions {
  title? : string;
  size? : number;
  isCollapsed? : boolean
};
export function lazyCustomSearchbox() {
  // Wait for the 'Searchbox' promise to resolve, then return the 'CustomSearchbox' implementation.
  return Coveo.load<IComponentDefinition>('Searchbox').then((Searchbox) => {
    class CustomSearchbox extends Searchbox {
      static ID = 'CustomSearchbox';
      static options ICustomSearchboxOptions = _.extend({}, {
        title : ComponentOptions.buildStringOption(),
        size : ComponentOptions.buildNumberOption({ min : 0 }),
        isCollapsed : ComponentOptions.buildBooleanOption({ defaultValue : false })
      };
      constructor(public element: HTMLElement, public options?: ICustomComponentOptions, bindings?: IComponentBindings) {
        super(element, ComponentOptions.initComponentOptions(element, CustomSearchbox, options), bindings);
      };
    };
 
    Coveo.Initialization.registerAutoCreateComponent(CustomSearchbox);
    return CustomSearchbox;
  });
};
// Register the 'CustomSearchbox' lazy component using the previously defined function.
Coveo.LazyInitialization.registerLazyComponent('CustomSearchbox', lazyCustomSearchbox);

Or, you could achieve the same result in JavaScript:

function lazyCustomSearchbox() {
  // Wait for the 'Searchbox' promise to resolve, then return the 'CustomSearchbox' implementation.
  return Coveo.load('Searchbox').then(function(Searchbox) {
    var CustomSearchbox = (function(_super) {
      __extends(CustomSearchbox, _super);
      function CustomSearchbox(element, options, bindings) {
        _super.call(this, element, ComponentOptions.initComponentOptions(element, CustomSearchbox, options), bindings);
        this.element = element;
        this.options = options;
        this.bindings = bindings;
      }
      CustomSearchbox.ID = 'CustomSearchbox';
      CustomSearchbox.options = _extend({}, {
        title : ComponentOptions.buildStringOption(),
        size : ComponentOptions.buildNumberOption({ min : 0 }),
        isCollapsed : ComponentOptions.buildBooleanOption({ defaultValue : false })
      }, Coveo.Searchbox.options);
      return CustomSearchbox;
    }(Searchbox));
    Coveo.Initialization.registerAutoCreateComponent(CustomSearchbox);
    return CustomSearchbox;
  });
}
// Register the 'CustomSearchbox' lazy component using the previously defined function.
Coveo.LazyInitialization.registerLazyComponent('CustomSearchbox', lazyCustomSearchbox);

Fixing Code Chunks Loading Path Issues

Coveo JavaScript Search Framework 2.4094.8 (May 2018) 

In some setups, the framework cannot automatically detect the path from which to load the code. When this issue occurs, the following error appears in the browser console:

Cannot load chunk for [ ... ] You may need to add the coveo-script class on the script tag that includes the Coveo framework.

As stated in the error message, you need to add the CSS class coveo-script on the HTML script element that includes the Coveo framework.

<script class="coveo-script" src="js/CoveoJsSearch.Lazy.js"></script>

You need to do this if the script tag that includes the Coveo script is added dynamically to your page and you need to support IE11. This error can also occur when using IE11 and when any script in the page is added dynamically.

In older versions, you will get this error message instead:

Cannot load chunk for [ ... ] You may need to configure the paths of the resources using Coveo.configureRessourceRoot. Current path is [ ... ].

As stated in the error message, you should use the Coveo.configureRessourceRoot function to configure the correct path. The path given as a string argument should have a trailing slash (/). The path can be absolute or relative to the page from which the Coveo script is included.

Suppose your search page has the following directory tree:

  • lib
    • JSUI
      • CoveoJsSearch.Lazy.min.js
  • page - index.html

If you want to use an absolute path, you need to do Coveo.configureRessourceRoot('/lib/JSUI/'). If you rather want to use a relative path, you must do Coveo.configureRessourceRoot('../lib/JSUI/') instead.