Guides / Building Search UI / Going further

Improve Performance for Vue InstantSearch

Preparing the connection to Algolia

When sending the first network request to a domain, a security handshake must happen, consisting of several round trips between the client and the Algolia server. If the handshake first happened when the user typed their first keystroke, the speed of that first request would be significantly slower.

You can use a preconnect link to carry out the handshake immediately after the page has loaded, before any user interaction. To do this, add a link tag with your Algolia domain in the head of your page.

1
2
3
4
<link crossorigin href="https://YOUR_APPID-dsn.algolia.net" rel="preconnect" />

<!-- for example: -->
<link crossorigin href="https://B1G2GM9NG0-dsn.algolia.net" rel="preconnect" />

Mitigate the impact of a slow network on your application

Since Algolia is a hosted search API, the search experience is affected if the network is slow. This guide shows you how to make the user’s perception of search better despite adverse network conditions.

Adding a loading indicator

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

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

At this point, the user is really wondering what’s 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. This example shows how to 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
30
31
<!-- 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 search stalls. If network conditions are optimal, users won’t see this message.

Debouncing

Another way of improving the perception of performance is to try to prevent lag. Although the default InstantSearch experience of generating one query per keystroke is usually desirable, this can lead to a lag in the worst network conditions because browsers can only make a limited number of parallel requests. By reducing the number of requests done, you can prevent this lag.

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
46
<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 {
      timerId: null,
      localQuery: '',
    }
  },
  destroyed() {
    if (this.timerId) {
      clearTimeout(this.timerId)
    }
  },
  computed: {
    query: {
      get() {
        return this.localQuery
      },
      set(val) {
        this.localQuery = val
        if (this.timerId) {
          clearTimeout(this.timerId)
        }
        this.timerId = setTimeout(() => {
          this.state.refine(this.localQuery)
        }, this.delay)
      },
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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 supports dead code elimination through tree shaking but you must follow a few rules for it to work:

  • Bundle your code using a module bundler that supports tree shaking via the sideEffects property in package.json, such as Rollup or webpack 4+.
  • Make sure to pick the ES module build of Vue InstantSearch by targeting the module field in package.json (resolve.mainFields option in webpack, mainFields option in @rollup/plugin-node-resolve). This is the default configuration in most popular bundlers, so you shouldn’t need to change anything unless you have a custom configuration.
  • Keep Babel or other transpilers from transpiling ES6 modules to CommonJS modules. Tree shaking is much less optimal on CommonJS modules, so it’s better to let your bundler handle modules by itself.

If you’re using Babel, you can configure babel-preset-env not to process ES6 modules:

1
2
3
4
5
6
7
8
9
10
11
// babel.config.js
module.exports = {
  presets: [
    [
      'env',
      {
        modules: false,
      },
    ],
  ],
}

or if you are using the TypeScript compiler (tsc):

1
2
3
4
5
6
// tsconfig.json
{
  "compilerOptions": {
    "module": "esnext",
  }
}
  • Import only what you need, and avoid using the plugin (Vue.use(VueInstantSearch)). Doing so imports all the widgets, even the ones you don’t use.

    Instead, individually import and register each InstantSearch widget within components:

    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
    
    <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>
    

    With this approach, only the manually imported widgets end up in the production build. Tree shaking removes the rest.

    You can also register InstantSearch widgets at the application level:

    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),
    })
    

Troubleshooting

To ensure tree shaking is working, you can try to import Vue InstantSearch in your project without using it.

1
import 'vue-instantsearch' // Unused import

Build your application, then look for the unused code in your final bundle (for example, “InstantSearch”). If tree shaking works, you shouldn’t find anything.

Caching

Caching by default (and how to turn it off)

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’s a very convenient feature, sometimes it’s 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 front end of your application to be updated to reflect that change (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
34
35
<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 can also set up an interval that determines how often the cache is cleared. This method ensures that the cache is cleared on a regular basis. You should use this approach if you can’t 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
44
45
<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 aren’t limited by a fixed “search quota”. That being said, they’re limited by the maximum QPS and the operations limit of your plan.

Every key press in InstantSearch using the SearchBox counts as one operation. Then, depending on the widgets you add to your search interface, you may have more operations being counted on each keystroke. For example, if you have a search interface with a ais-search-box, a ais-menu, and a ais-refinement-list, then every keystroke triggers one operation. But as soon as a user refines the ais-menu or ais-refinement-list, it triggers a second operation on each keystroke.

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

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

Did you find this page helpful?