> ## Documentation Index
> Fetch the complete documentation index at: https://algolia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Improve performance for Vue InstantSearch

> Learn how to optimize your Vue InstantSearch app.

export const Records = () => <Tooltip tip="A record is a searchable object in an Algolia index. Each record consists of named attributes." cta="Algolia records" href="/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records">
    records
  </Tooltip>;

export const Index = () => <Tooltip tip="An Algolia index is a searchable dataset that consists of records and configuration settings. These settings define how the records are searched and ranked.">
    index
  </Tooltip>;

Algolia is fast by default.
But network speed and bandwidth can vary.
This page lists best practices you can implement to adapt to your users' network conditions.

## Prepare 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 users typed their first keystroke, the speed of that first request would be slower.

Use a [preconnect link](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/preconnect) to carry out the handshake after loading the page but before any user interaction.
To do this, add a link tag with your Algolia domain in the `head` of your page.

```html HTML icon=code-xml theme={"system"}
<link crossorigin href="https://YOUR_APPID-dsn.algolia.net" rel="preconnect" />

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

## Add a loading indicator

Consider a user accessing your app in a subway:

1. They type some characters
2. Nothing happens
3. They wait, but still, nothing happens

However, you can enhance the user experience by displaying a loading indicator to indicate something is happening.

To display a loading indicator in the `ais-search-box`, use the [`show-loading-indicator`](/doc/api-reference/widgets/search-box/vue#param-show-loading-indicator) option. The indicator will display slightly after the last query has been sent to Algolia. Change the duration of the delay with [`stalled-search-delay`](/doc/api-reference/widgets/instantsearch/vue#param-stalled-search-delay) (on the `ais-instant-search` widget).

For example:

```vue Vue icon=code theme={"system"}
<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

You can also use the loading indicator in [custom widgets](/doc/guides/building-search-ui/widgets/create-your-own-widgets/vue). The following 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.

<CodeGroup>
  ```vue Vue 3 theme={"system"}
  <!-- components/LoadingIndicator.vue -->
  <template>
    <div v-if="state && state.searchMetadata.isSearchStalled">
      <p>Loading…</p>
    </div>
  </template>

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

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

  ```vue Vue 2 theme={"system"}
  <!-- 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>
  ```
</CodeGroup>

```vue Vue icon=code theme={"system"}
<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>
```

## Debouncing

Another way of improving the perception of performance is by preventing lag.
InstantSearch generates one query per keystroke by default.
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, you can prevent this lag.

Debouncing prevents unnecessary requests by delaying them until a timeout period has passed.

Implement debouncing at the `ais-search-box` level with the [`connectSearchBox`](/doc/api-reference/widgets/search-box/js#customize-the-ui-with-connectsearchbox) connector. For example:

<CodeGroup>
  ```vue Vue 3 theme={"system"}
  <template>
    <input type="search" v-model="query" />
  </template>

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

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

  ```vue Vue 2 theme={"system"}
  <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>
  ```
</CodeGroup>

```vue Vue icon=code theme={"system"}
<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>
```

Find the complete source code on [GitHub](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/debounced-search-box).

<Info>
  This code has been specifically created for Vue 2. Some [modifications](https://v3-migration.vuejs.org/) may be required for it to work correctly in Vue 3.
</Info>

### Select a debounce delay

The optimal debouncing delay should match your audience's typing speed
(typically 30 words per minute (WPM) on mobile devices and 40 WPM on desktop devices).
If the delay is too short, users still see flashes of content.
If the delay is too long, the time between key presses and results becomes too long.

200 ms is the preferred debounce delay.
Delays of over 300 ms will start degrading the user experience.

## Optimize build size

InstantSearch supports [dead code elimination through tree shaking](https://webpack.js.org/guides/tree-shaking/), but you must follow these rules for it to work:

* Bundle your code using a module bundler that supports tree shaking with the `sideEffects` property in `package.json`, such as [Rollup](https://rollupjs.org/) or [webpack 4+](https://webpack.js.org).
* Ensure you pick the ES module build of InstantSearch by targeting the `module` field in `package.json` ([`resolve.mainFields` option in webpack](https://webpack.js.org/configuration/resolve/#resolvemainfields), [`mainFields` option in `@rollup/plugin-node-resolve`](https://github.com/rollup/plugins/tree/master/packages/node-resolve#mainfields)). This is the default configuration in most popular bundlers: you only need to change something if you have a custom configuration.
* Keep [Babel](https://babeljs.io) 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 itself.

If you're using Babel, you can configure [`babel-preset-env`](https://babeljs.io/docs/en/babel-preset-env) not to process ES6 modules:

```js JavaScript icon=code theme={"system"}
// babel.config.js
module.exports = {
  presets: [
    [
      "env",
      {
        modules: false,
      },
    ],
  ],
};
```

If you're using the [TypeScript compiler (`tsc`)](https://www.typescriptlang.org/docs/handbook/2/basic-types.html#tsc-the-typescript-compiler):

```jsonc JSON icon=braces theme={"system"}
// tsconfig.json
{
  "compilerOptions": {
    "module": "esnext",
  }
}
```

Import only what you need, and avoid using the plugin (`Vue.use(VueInstantSearch)`) plugin. Doing so imports all the widgets, even the ones you don't use.
Instead, individually import and register each InstantSearch widget within components:

<CodeGroup>
  ```vue Vue 3 theme={"system"}
  <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 { liteClient as algoliasearch } from 'algoliasearch/lite';
  import {
    AisInstantSearch,
    AisSearchBox,
    AisHits,
  } from 'vue-instantsearch/vue3/es';

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

  ```vue Vue 2 theme={"system"}
  <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 { liteClient as algoliasearch } from 'algoliasearch/lite';
  import { AisInstantSearch, AisSearchBox, AisHits } from 'vue-instantsearch';

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

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

You can also register InstantSearch widgets at the app level:

<CodeGroup>
  ```vue Vue 3 theme={"system"}
  import Vue from 'vue';
  import { AisInstantSearch, AisSearchBox } from 'vue-instantsearch/vue3/es';
  import App from './App.vue';

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

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

  ```vue Vue 2 theme={"system"}
  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),
  });
  ```
</CodeGroup>

### Troubleshooting

To check if tree shaking works, try to import InstantSearch into your project without using it.

```js JavaScript icon=code theme={"system"}
import "vue-instantsearch"; // Unused import
```

Build your app, 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. This cache only persists during the current page session, and as soon as the page reloads, the cache clears.

If users type a search (or part of it) that's already been entered, the results will be retrieved from the cache instead of requesting them from Algolia, making the app much faster.

While it's a convenient feature, sometimes you may want to clear the cache and make a new request to Algolia.
For instance, when changes are made to some <Records /> in your <Index />, you should update your app's frontend to reflect that change (and avoid displaying stale results retrieved from the cache).

The `refresh` function, available for custom connectors, lets you clear the cache and trigger a new search.

The `refresh` function, available for custom connectors, lets you clear the cache and trigger a new search.

### When to discard the cache

Consider discarding the cache when your app's data is updated by:

* Your users (for example, in a dashboard). In this case, refresh the cache based on an app state, such as the last user modification.
* Another process you don't manage (for example, a cron job that updates users inside Algolia). In this case, you should refresh your app's cache periodically.

### Refresh the cache triggered by a user action

The following code triggers a refresh based on a user action (such as adding a new product or clicking a button).

<CodeGroup>
  ```vue Vue 3 theme={"system"}
  <template>
    <button @click="refresh">refresh</button>
  </template>

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

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

  ```vue Vue 2 theme={"system"}
  <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>
  ```
</CodeGroup>

Use it within your app as follows:

```vue Vue icon=code theme={"system"}
<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>
```

Find the complete source code on [GitHub](https://github.com/algolia/doc-code-samples/tree/master/vue-instantsearch/refresh).

<Info>
  This code has been specifically created for Vue 2. Some [modifications](https://v3-migration.vuejs.org/) may be required for it to work correctly in Vue 3.
</Info>

### Refresh the cache periodically

You can set an interval to determine how often the app clears the cache.
Use this approach if you can't trigger cache clearance based on user actions.

<CodeGroup>
  ```vue Vue 3 theme={"system"}
  <template>
    <button @click="refresh">refresh</button>
  </template>

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

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

  ```vue Vue 2 theme={"system"}
  <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>
  ```
</CodeGroup>

If you need to wait for an action from Algolia, use [`waitTask`](/doc/libraries/sdk/v1/methods/wait-task) to avoid refreshing the cache before the task completes.

### Turn off the cache

If you need the most current data and the performance impact of this isn't an issue, [turn off caching](/doc/libraries/sdk/caching#requests-and-responses).

```js JavaScript icon=code theme={"system"}
import { liteClient as algoliasearch } from "algoliasearch/lite";
import { createNullCache } from "@algolia/cache-common";

const searchClient = algoliasearch(
  "ALGOLIA_APPLICATION_ID",
  "ALGOLIA_SEARCH_API_KEY",
  {
    // Disable caching for completed requests
    responsesCache: createNullCache(),
    // Disable caching for in-flight requests
    requestsCache: createNullCache(),
  },
);
```

## Queries per second (QPS)

Search operations aren't limited by a fixed "search quota".
Instead, they're limited by your plan's [maximum QPS](https://support.algolia.com/hc/en-us/articles/4406975224721) and
[operations limit](https://support.algolia.com/hc/en-us/articles/18138875086865).

Every keystroke in InstantSearch using the `ais-search-box` 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 an `ais-search-box`, an `ais-menu`, and an `ais-refinement-list`,
then every keystroke triggers one operation.
Upon each user change to an `ais-menu` or `ais-refinement-list`, a new operation is executed.

If you experience QPS limitations, consider implementing [a debounced `ais-search-box`](#debouncing).

For more information, see:

* [`ais-search-box`](/doc/api-reference/widgets/search-box/vue)
* [`ais-menu`](/doc/api-reference/widgets/menu/vue)
* [`ais-refinement-list widget`](/doc/api-reference/widgets/refinement-list/vue)
