# How to Fetch Data on Initial Page Load with Vue 3 CDN Without Creating a Separate Instance

> Efficiently fetch data on initial page load with Vue 3 CDN. Avoid extra instances by using native JavaScript before creating your app and passing data to the root component.

- Repository: [Vue/core](https://github.com/vuejs/core)
- Tags: how-to-guide
- Published: 2026-02-21

---

**Fetch data using native JavaScript before calling `Vue.createApp()`, then pass the resolved data to your root component via `setup`, props, or `provide`/`inject` to avoid the overhead of a secondary Vue instance.**

When using the Vue 3 global build from a CDN, you can leverage standard JavaScript patterns to initialize data before the framework boots. According to the `vuejs/core` repository, the global build exposes a single `Vue` object that allows you to run arbitrary code prior to app instantiation, making this pattern both efficient and idiomatic.

## Why Fetch Before Creating the Vue App?

The global build of Vue 3 ([`vue.global.js`](https://github.com/vuejs/core/blob/main/vue.global.js) or [`vue.global.prod.js`](https://github.com/vuejs/core/blob/main/vue.global.prod.js)) is designed to work without a bundler. As documented in [`packages/vue/README.md`](https://github.com/vuejs/core/blob/main/packages/vue/README.md), this build inlines all core packages and exposes the framework via a global `Vue` variable.

Because the CDN bundle is just a plain JavaScript file, you can execute any logic—such as network requests—before invoking `Vue.createApp`. This approach eliminates the need for a separate "loader" Vue instance or complex async component wrappers. The fetch operation starts immediately when the script parses, ensuring the data is ready (or in flight) by the time the component renders.

## Step-by-Step Implementation

### Start the Fetch Request Immediately

Begin by initiating the fetch request in a standard `<script>` tag before defining your Vue components. This ensures the network request starts as early as possible in the page lifecycle.

```html
<script>
  // Start fetching immediately - no Vue instance needed
  const dataPromise = fetch('https://api.example.com/items')
    .then(res => res.json());
</script>

```

### Define the Root Component with Async Setup

Use an async `setup` function in your root component to await the promise. Because the fetch was initiated earlier, the promise resolves instantly if the network request has already completed. As implemented in [`packages/runtime-core/src/component.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/component.ts), Vue handles async `setup` functions seamlessly when the data is already available.

```javascript
const App = {
  async setup() {
    // Awaits the existing promise - no additional network round-trip
    const items = await dataPromise;
    
    return { items };
  },
  template: `
    <div>
      <h1>Items</h1>
      <ul>
        <li v-for="item in items" :key="item.id">{{ item.name }}</li>
      </ul>
    </div>
  `
};

```

### Mount the App After Initiating the Fetch

Finally, create and mount the Vue application after the fetch has been initiated. This timing ensures the `setup` function can immediately await the resolved data.

```javascript
// Create and mount after fetch is in flight
Vue.createApp(App).mount('#app');

```

## Alternative Patterns for Passing Data

### Using Provide and Inject

For applications where nested components need access to the initial data, use `provide` at the root level. This pattern, supported by the reactivity system in [`packages/runtime-core/src/apiLifecycle.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/apiLifecycle.ts), makes data available to any descendant without prop drilling.

```javascript
dataPromise.then(items => {
  const app = Vue.createApp({
    setup() {
      Vue.provide('initialItems', items);
      return {};
    },
    template: '<router-view />'
  });
  
  app.mount('#app');
});

```

Child components can then access the data via `inject`:

```javascript
const ChildComponent = {
  setup() {
    const items = Vue.inject('initialItems');
    return { items };
  }
};

```

### Passing as Props to the Root Component

You can also pass the resolved data as the second argument to `createApp`, which sets the root component's props. This method is useful when you want the root component to receive data exactly like a standard prop.

```javascript
fetch('https://api.example.com/config')
  .then(res => res.json())
  .then(config => {
    Vue.createApp(
      {
        props: ['initialConfig'],
        template: '<div>{{ initialConfig }}</div>'
      },
      { initialConfig: config }
    ).mount('#app');
  });

```

## Key Source Files and Implementation Details

Understanding the internal architecture confirms why these patterns work efficiently:

- **[`packages/vue/README.md`](https://github.com/vuejs/core/blob/main/packages/vue/README.md)** – Documents the global build configuration that exposes the `Vue` global object and confirms that the CDN bundle inlines all core packages, ensuring consistent internals without a bundler.

- **[`packages/runtime-core/src/component.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/component.ts)** (lines 880-904) – Implements the async `setup` handling and Suspense boundaries. This confirms that awaiting a pre-fetched promise inside `setup` works correctly without requiring additional wrapper components.

- **[`packages/runtime-core/src/apiLifecycle.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/apiLifecycle.ts)** (lines 58-61) – Contains the implementation of `provide` and `inject`, verifying that data provided at the root level is reactive and available to all descendants.

- **[`packages/runtime-core/src/helpers/useId.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/helpers/useId.ts)** (lines 23-30) – Demonstrates internal detection of async `setup` boundaries, confirming that the framework properly tracks component initialization state when using async patterns.

## Summary

- **Fetch early, create app late**: Initiate data fetching with native JavaScript before calling `Vue.createApp()` to eliminate loading states and secondary instances.
- **Use async `setup`**: Await the pre-fetched promise inside your root component's `setup` function; the data resolves synchronously if the fetch completed before mount.
- **Leverage `provide`/`inject`**: For deep component trees, use `Vue.provide()` at the root level to distribute initial data without prop drilling.
- **Global build advantages**: The Vue 3 CDN bundle ([`vue.global.prod.js`](https://github.com/vuejs/core/blob/main/vue.global.prod.js)) exposes a single `Vue` object that supports all these patterns without requiring a build step.

## Frequently Asked Questions

### Can I use `async setup` without `<Suspense>` when fetching data this way?

Yes. When you fetch data *before* creating the app, the promise is already resolved (or resolving) by the time `setup` runs. According to the source in [`packages/runtime-core/src/component.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/component.ts), Vue only requires `<Suspense>` when the component itself initiates the async operation. Since you're awaiting an existing promise that likely already has data, the component renders immediately without a suspense boundary.

### Is it better to pass data as props or use `provide`/`inject`?

It depends on your component architecture. **Props** work best when the root component directly consumes the data or when you have a shallow component tree. **`provide`/`inject`** is more efficient for deep nesting or when multiple branches need the same initial data, as it eliminates prop drilling. Both methods are reactive and officially supported by the runtime core as documented in [`packages/runtime-core/src/apiLifecycle.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/apiLifecycle.ts).

### What happens if the fetch fails before the app is created?

Since the fetch runs in plain JavaScript outside Vue's error handling, you should implement standard Promise error handling (`.catch()` or `try/catch` in an async IIFE) before mounting the app. If the fetch fails, you can either mount an error-specific component, display a static error message, or retry the request. The key advantage is that you control the error state *before* Vue initializes, allowing you to decide whether to mount the app at all based on the fetch result.

### Does this pattern work with the Composition API and `<script setup>`?

Yes. When using the Composition API with `<script setup>` (in a build setup), you would import the fetched data from a module. In the CDN context without a build step, you assign the data to a global variable or closure that your `setup` function can access. The async `setup` pattern works identically whether you use the Options API or Composition API, as confirmed by the component initialization logic in [`packages/runtime-core/src/component.ts`](https://github.com/vuejs/core/blob/main/packages/runtime-core/src/component.ts).