---
title: "Script Loading"
description: "Load external scripts with useScript(), shared instances, triggers, API resolution, callbacks, and resource warmup."
canonical_url: "https://unhead.unjs.io/docs/head/guides/core-concepts/loading-scripts"
last_updated: "2026-08-11T00:39:05.265Z"
---

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

```ts
import { useScript } from '@unhead/dynamic-import'

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.

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

```ts
import { useScript } from '@unhead/dynamic-import'

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.

```ts
import { useScript } from '@unhead/dynamic-import'

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.

```ts
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).

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

You can also add a hint later:

```ts
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](/docs/head/api/composables/use-script): Complete types and lifecycle
- [DOM Event Handling](/docs/head/guides/core-concepts/dom-event-handling): Direct resource handlers
