Concepts / Building Search UI / Improve performance
Oct. 11, 2019

Improve Performance

You are reading the documentation for Vue InstantSearch v2. Read our migration guide to learn how to upgrade from v1 to v2. You can still find the v1 documentation here.

Mitigate the impact of slow network in your search application.

Algolia is a hosted search API. This means that if the network is slow, the search experience will be impacted. In this guide, you will see how to make the perception of search better even if the network shows some downtime.

Adding a loading indicator

Imagine a user using your search in a subway, by default this is the kind of experience they will get:

  • type some characters
  • nothing happens
  • still waiting, still nothing

At this point, the user is really wondering what is happening. You can start enhancing this experience by providing a visual element to hint that something is happening: a loading indicator.

Vue InstantSearch provides an option on ais-search-box called show-loading-indicator. The indicator appears inside the ais-search-box. It’s also triggered a little after the last query has been sent to Algolia. This prevents the element to flicker.

The delay can be configured using the stalled-search-delay on the ais-instant-search widget.

Here is an example:

1
2
3
4
5
6
7
8
9
<template>
  <ais-instant-search
    index-name="instant_search"
    :search-client="searchClient"
    :stalled-search-delay="200"
  >
    <ais-search-box show-loading-indicator />
  </ais-instant-search>
</template>

Make your own loading indicator

The mechanism of the loading indicator is also available through custom widgets. Here we will make one that shows the text “Loading…” if the search request takes longer than expected:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- components/LoadingIndicator.vue -->
<template>
  <div v-if="state && state.searchMetadata.isSearchStalled">
    <p>Loading…</p>
  </div>
</template>

<script>
import { createWidgetMixin } from 'vue-instantsearch';

const connectSearchMetaData = (renderFn, unmountFn) => (widgetParams = {}) => ({
  init() {
    renderFn({ searchMetadata: {} }, true);
  },

  render({ searchMetadata }) {
    renderFn({ searchMetadata }, false);
  },

  dispose() {
    unmountFn();
  },
});

export default {
  name: 'AisStateResults',
  mixins: [createWidgetMixin({ connector: connectSearchMetaData })],
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
  <ais-instant-search
    index-name="instant_search"
    :search-client="searchClient"
    :stalled-search-delay="200"
  >
    <ais-search-box />
    <app-loading-indicator />
  </ais-instant-search>
</template>

<script>
import AppLoadingIndicator from './components/LoadingIndicator';

export default {
  components: {
    AppLoadingIndicator,
  },
};
</script>

This example shows how to make a custom component that writes Loading... when the search is stalled and nothing appears. Because of the delay introduced, users with optimal conditions will not be bothered by the message. If the user is not searching in those conditions, we provide some information about what is going on underneath.

Debouncing

Another way of thinking about performance perception is to try to prevent some of the lagging effect. The InstantSearch experience generates one query per keystroke. While this is normally desirable, in the worst of conditions this can lead to congestion because browsers can only make a limited amount of requests in parallel. By reducing the amount of requests done, we can prevent this effect.

Debouncing is a way to limit the number of requests and avoid processing non-necessary ones by avoiding sending requests before a timeout.

There is no built-in solution to debounce in Vue InstantSearch. You can implement it at the ais-search-box level with the help of the searchBox connector. Here is an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<template>
  <input
    type="search"
    v-model="query"
  />
</template>

<script>
import { connectSearchBox } from 'instantsearch.js/es/connectors';
import { createWidgetMixin } from 'vue-instantsearch';

export default {
  mixins: [createWidgetMixin({ connector: connectSearchBox })],
  props: {
    delay: {
      type: Number,
      default: 200,
      required: false,
    },
  },
  data() {
    return {
      localQuery: '',
    };
  },
  destroyed() {
    if (this.timerId) {
      clearTimeout(this.timerId);
    }
  },
  computed: {
    query: {
      get() {
        return this.localQuery;
      },
      set(val) {
        this.localQuery = val;
        this.timeoutId = setTimeout(() => {
          this.state.refine(this.localQuery);
        }, this.delay);
      },
    },
  },
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
  <ais-search-box
    index-name="instant_search"
    :search-client="searchClient"
  >
    <app-debounced-search-box :delay="200" />
  </ais-search-box>
</template>

<script>
import AppDebouncedSearchBox from './components/DebouncedSearchBox.js';

export default {
  components: {
    AppDebouncedSearchBox,
  },
};
</script>

You can find the source code on GitHub.

Optimize build size

Vue InstantSearch is by default optimized for build optimization like tree shaking. However, if you use the plugin (Vue.use(VueInstantSearch)), you will be importing all of the components.

An alternative approach is to register components when we need them:

1
2
3
4
5
6
7
8
9
10
11
import Vue from 'vue';
import { AisInstantSearch, AisSearchBox } from 'vue-instantsearch';
import App from './App.vue'

Vue.component(AisInstantSearch.name, AisInstantSearch);
Vue.component(AisSearchBox.name, AisSearchBox);

new Vue({
  el: '#app',
  render: h => h(App)
})

With this approach, only the four manually imported components will be part of your production build. The other components will be removed after tree shaking.

It’s also possible to do this in your component itself:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
  <div id="app">
    <ais-instant-search :search-client="searchClient" index-name="indexName">
      <ais-search-box></ais-search-box>
      <ais-hits></ais-hits>
    </ais-instant-search>
  </div>
</template>

<script>
import algoliasearch from 'algoliasearch/lite';
import { AisInstantSearch, AisSearchBox, AisHits } from 'vue-instantsearch';

export default {
  components: {
    AisInstantSearch,
    AisSearchBox,
    AisHits,
  },
  data() {
    return {
      searchClient: algoliasearch(
        'YourApplicationID',
        'YourAdminAPIKey'
      ),
    };
  },
};
</script>

Naming

When using Vue.use(InstantSearch) all components are registered with the ais- prefix, which stands for Algolia InstantSearch. Example: ais-search-box.

When manually importing components, you can change that naming convention and assign a custom tag name.

Caching

Caching by default (and how to get rid of it)

By default, Algolia caches the search results of the queries, storing them locally in the cache. If the user ends up entering a search (or part of it) that has already been entered previously, the results will be retrieved from the cache, instead of requesting them from Algolia, making the application much faster. Note that the cache is an in-memory cache, which means that it only persist during the current page session. As soon as the page reloads the cache is cleared.

While it is a very convenient feature, in some cases it is useful to have the ability to clear the cache and make a new request to Algolia. For instance, when changes are made on some records on your index, you might want the frontend side of your application to be updated to reflect that change (in order to avoid displaying stale results retrieved from the cache).

To do so, there is a function refresh available for custom connectors, which clears the cache and triggers a new search.

There are two different use cases where you would want to discard the cache:

  • your application data is being directly updated by your users (for example, in a dashboard). In this use case you would want to refresh the cache based on some application state such as the last modification from the user.

  • your application data is being updated by another process that you don’t manage (for example a CRON job that updates users inside Algolia). For this you might want to periodically refresh the cache of your application.

Refresh the cache triggered by an action from the user

If you know that the cache needs to be refreshed conditionally after a specific event, then you can trigger the refresh based on a user action (adding a new product, clicking on a button for instance).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
  <button @click="refresh">refresh</button>
</template>

<script>
import { createWidgetMixin } from 'vue-instantsearch';

const connectRefresh = (renderFn, unmountFn) => (widgetParams = {}) => ({
  init() {
    renderFn({ refresh() {} }, true);
  },

  render({ instantSearchInstance }) {
    const refresh = instantSearchInstance.refresh.bind(instantSearchInstance);

    renderFn({ refresh }, false);
  },

  dispose() {
    unmountFn();
  },
});

export default {
  name: 'AisStateResults',
  mixins: [createWidgetMixin({ connector: connectRefresh })],
  methods: {
    refresh() {
      this.state.refresh();
    },
  },
};
</script>

And use it within your app:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
  <ais-instant-search
    index-name="instant_search"
    :search-client="searchClient"
    :stalled-search-delay="200"
  >
    <ais-search-box />
    <app-refresh />
    <ais-hits />
  </ais-instant-search>
</template>

<script>
import AppRefresh from './components/Refresh.js';

export default {
  components: {
    AppRefresh,
  },
};
</script>

You can find the source code on GitHub.

Refresh the cache periodically

You also have the option to setup an given period of time that will determine how often the cache will be cleared. This method will ensure that the cache is cleared on a regular basis. You should use this approach if you cannot use a user action as a specific event to trigger the clearing of the cache.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<template>
  <button @click="refresh">refresh</button>
</template>

<script>
import { createWidgetMixin } from 'vue-instantsearch';

const connectRefresh = (renderFn, unmountFn) => (widgetParams = {}) => ({
  init() {
    renderFn({ refresh: {} }, true);
  },

  render({ instantSearchInstance }) {
    const refresh = instantSearchInstance.refresh.bind(instantSearchInstance);
    renderFn({ refresh }, false);
  },

  dispose() {
    unmountFn();
  },
});

export default {
  props: {
    delay: {
      type: Number,
      default: 10000, // (10 seconds)
    },
  },
  name: 'AisStateResults',
  mixins: [createWidgetMixin({ connector: connectRefresh })],
  mounted() {
    this.timerId = setInterval(() => {
      this.state.refresh();
    }, this.delay);
  },
  destroyed() {
    if (this.timerId) {
      clearInterval(this.timerId);
    }
  },
};
</script>

Note that if you need to wait for an action from Algolia, you should use waitTask to avoid refreshing the cache too early. You can find the source code on GitHub.

Queries Per Second (QPS)

Search operations are limited by the maximum QPS (the allowed number of queries performed per second) of the plan.

Every time you press a key in InstantSearch using the SearchBox, we count one operation. Then, depending on the widgets you will be adding to your search interface, you may have more operations being counted on each keystroke. For example, if you have a search made out of a ais-search-box, a ais-menu, and a ais-refinement-list, then every keystroke will trigger one operation. But as soon as a user refines the ais-menu or ais-refinement-list, it will trigger a second operation on each keystroke.

A good rule to keep in mind is that most search interfaces using InstantSearch will trigger one operation per keystroke. Then every refined widget (clicked widget) will add one more operation to the total count.

In case you have issue with the QPS you can consider implement a debounced ais-search-box.

Did you find this page helpful?