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

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 or vue.global.prod.js) is designed to work without a bundler. As documented in 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.

<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, Vue handles async setup functions seamlessly when the data is already available.

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.

// 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, makes data available to any descendant without prop drilling.

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:

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.

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 – 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 (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 (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 (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) 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, 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.

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →