---
title: "Script Loading · Unhead"
canonical_url: "https://unhead.unjs.io/docs/vue/head/guides/core-concepts/loading-scripts"
last_updated: "2026-09-16T20:48:43.078Z"
meta:
  description: "Load external scripts with useScript(), shared instances, triggers, API resolution, callbacks, and resource warmup."
  "og:description": "Load external scripts with useScript(), shared instances, triggers, API resolution, callbacks, and resource warmup."
  "og:title": "Script Loading · Unhead"
---

Home

`
Unhead on GitHub

Switch to VueSwitch to TypeScriptSwitch to ReactSwitch to SvelteSwitch to Solid.jsSwitch to AngularSwitch to Nuxt

**Core Concepts**

# **Script Loading**

[Copy for LLMs](https://raw.githubusercontent.com/unjs/unhead/refs/heads/main/docs/head/1.guides/1.core-concepts/9.loading-scripts.md)

[`**useScript()**`](https://unhead.unjs.io/docs/head/api/composables/use-script) manages an external script's lifecycle and returns a shared script instance.

```
import { useScript } from '@unhead/vue'

const script = useScript('https://example.com/sdk.js')
script.onLoaded(() => console.log('SDK loaded'))
script.onError(error => console.error('SDK failed', error))
```

## Script identity

Scripts are shared within one Unhead instance. The identity is the input's `**key**`, then its `**src**`, then a string `**innerHTML**` value. Repeated calls with the same identity return the cached `**ScriptInstance**` and do not add another script element.

```
useScript('https://example.com/sdk.js')
useScript('https://example.com/sdk.js') // same instance in this app/request
```

The cache is not process-global. Separate server requests or client apps with separate head instances do not share it.

## Default attributes

When `**load()**` adds the script entry, Unhead supplies:

- `**defer: true**`
- `**fetchpriority: 'low'**`
- `**crossorigin: 'anonymous'**` for absolute or protocol-relative URLs
- `**referrerpolicy: 'no-referrer'**` for absolute or protocol-relative URLs

Input attributes override these defaults. Unhead does not add `**async**` by default.

## Loading triggers

The `**trigger**` option accepts:

- `**'client'**` or `**undefined**`: use the integration's normal client loading behavior
- `**'server'**`: add the script during SSR
- `**'manual'**`: wait for an explicit `**load()**` call
- a promise: load when it fulfills with `**undefined**` or a truthy value
- a function: receive `**load**` and optionally return a cleanup function

```
import { useScript } from '@unhead/vue'

const manual = useScript('https://example.com/heavy-sdk.js', {
  trigger: 'manual'
})

async function enableFeature() {
  const api = await manual.load()
  if (!api)
    return // Loading failed, or the script was removed.
}

useScript('https://example.com/video-player.js', {
  trigger: (load) => {
    const element = document.querySelector('#video-container')
    element?.addEventListener('click', load, { once: true })
    return () => element?.removeEventListener('click', load)
  }
})
```

Framework integrations schedule the default trigger according to their mount lifecycle. Use an explicit trigger when timing must be identical across integrations.

## Resolving a script API

Provide `**resolve**` to tell Unhead how to obtain the API exposed by the script. The resolver runs on the client and may return a value or a promise.

```
import { useScript } from '@unhead/vue'

interface ExampleSdk {
  track: (event: string, data?: Record<string, unknown>) => void
}

const sdk = useScript<ExampleSdk>('https://example.com/sdk.js', {
  resolve: () => window.exampleSdk
})

sdk.onLoaded((api) => {
  api.track('page_view')
})
```

The legacy `**use**` option also resolves an API, but `**resolve**` additionally receives an `**AbortSignal**` and `**waitFor()**` helper for SDKs that become ready through a callback.

### Calling Through the Proxy

When `**resolve**` or `**use**` is configured, `**proxy**` records property access and function calls made before the API is ready. It replays function calls in order after resolution.

```
const sdk = useScript<ExampleSdk>('https://example.com/sdk.js', {
  resolve: () => window.exampleSdk
})

sdk.proxy.track('page_view')
```

Proxy functions intentionally return `**void**`, so they cannot provide synchronous results. Calls are never replayed if loading or API resolution fails. Without `**resolve**` or `**use**`, no API proxy is created; use `**onLoaded()**` only to observe the element load.

## Resource warmup

`**warmupStrategy**` accepts `**false**`, `**'preload'**`, `**'preconnect'**`, or `**'dns-prefetch'**`.

These hints do different amounts of work: `**dns-prefetch**` resolves an origin, `**preconnect**` also opens its network connection, and `**preload**` fetches a specific resource for the current navigation. Use the least expensive hint that matches what you know, and reserve preconnects for origins the page will use soon. See [**~~MDN's resource-hint guide~~**](https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/dns-prefetch) and [**~~web.dev's resource-hint guidance~~**](https://web.dev/learn/performance/resource-hints).

```
useScript('https://example.com/sdk.js', {
  trigger: 'manual',
  warmupStrategy: 'preconnect'
})
```

You can also add a hint later:

```
const script = useScript('https://example.com/sdk.js', {
  trigger: 'manual',
  warmupStrategy: false
})

script.warmup('preconnect')
await script.load()
```

`**preconnect**` and `**dns-prefetch**` are ignored for root-relative sources. A preload hint includes `**as="script"**`.

## Removing a Script

`**script.remove()**` disposes the managed script entry, removes its warmup entry, aborts pending lifecycle work, changes the status to `**removed**`, and deletes it from the head's script cache. A later call with the same source creates a new instance.

Most framework integrations do not remove an already loaded shared script when a component unmounts. Use `**scope: true**` in integrations that expose script scopes when you need consumer-owned callbacks and triggers, then call `**dispose()**` to release that scope without removing the shared script.

## See Also

- [**~~useScript() API~~**](https://unhead.unjs.io/docs/head/api/composables/use-script): Complete types and lifecycle
- [**~~DOM Event Handling~~**](https://unhead.unjs.io/docs/head/guides/core-concepts/dom-event-handling): Direct resource handlers

[Edit this page](https://github.com/unjs/unhead/edit/main/docs/head/1.guides/1.core-concepts/9.loading-scripts.md)

[Markdown For LLMs](https://raw.githubusercontent.com/unjs/unhead/refs/heads/main/docs/head/1.guides/1.core-concepts/9.loading-scripts.md)

**Did this page help you? **

[**DOM Event Handling** Attach client-side load, error, and body event handlers through useHead, with SSR hydration support.](https://unhead.unjs.io/docs/head/guides/core-concepts/dom-event-handling) [**Overview** Unified Vite and webpack plugins for Unhead. Build optimizations, inline script transpilation, tree-shaking, and useSeoMeta transforms in a single plugin call.](https://unhead.unjs.io/docs/head/guides/build-plugins/overview)

**On this page **

- [Script identity](#script-identity)
- [Default attributes](#default-attributes)
- [Loading triggers](#loading-triggers)
- [Resolving a script API](#resolving-a-script-api)
- [Resource warmup](#resource-warmup)
- [Removing a Script](#removing-a-script)
- [See Also](#see-also)