Loading Scripts
Introduction
The useScript
composable is built on top of useHead
, extending it with powerful script loading capabilities. Let's explore how to use it effectively.
Script Singleton Pattern
Scripts with the same src
(or key
) are only loaded once globally - they're shared across all components. Multiple calls to useScript
with the same script return the same instance.
Consider wrapping script initialization in composables for better reuse:
// useGoogleMaps.ts
export function useGoogleMaps() {
return useScript({
src: 'https://maps.googleapis.com/maps/api/js',
})
}
Default Behavior & Performance
By default, Unhead prioritizes performance and safe script loading:
- Scripts load after hydration for better performance
- Added performance attributes:
async
- Load without blockingdefer
- Execute in order after loadfetchpriority="low"
- Prioritize critical resources
- Added privacy attributes:
crossorigin="anonymous"
- Prevent cookie accessreferrerpolicy="no-referrer"
- Block referrer headers
Understanding Proxied Functions
The magic of SSR-safe script functions:
const { proxy } = useScript('/script.js')
// Works before script loads
proxy.gtag('event', 'page_view')
The proxy system queues function calls until the script loads. If the script never loads (e.g., blocked), the calls are dropped silently.
Benefits
- Works during SSR
- Resilient to script blocking
- Maintains function call order
- Load scripts anytime without breaking calls
Limitations
- Can't synchronously get return values
- May mask loading issues
- Harder to debug
For direct API access, await the script:
const { onLoaded } = useScript('/script.js')
onLoaded(({ gtag }) => {
gtag('event', 'page_view')
})
Script Triggers
Control when scripts load with triggers:
// Load after timeout
useScript('script.js', {
trigger: new Promise(resolve => setTimeout(resolve, 3000))
})
// Function trigger for client-side loading
useScript('script.js', {
trigger: (load) => {
// Called only on client, receive load callback
window.addEventListener('scroll', () => load())
}
})
// Manual loading
const { load } = useScript('script.js', {
trigger: 'manual'
})
load() // Load when ready
Warmup Strategies
Optimize loading with resource hints:
useScript('script.js', {
// Choose a strategy:
warmupStrategy: 'preload' | 'prefetch' | 'preconnect' | 'dns-prefetch'
})
Strategy Guide
preload
- Use for immediate loadingpreconnect
/dns-prefetch
- Use for cross-origin scripts loading within 10sfalse
- Disable warmup- Function - Dynamic strategy based on conditions
Manual Warmup
Pre-warm scripts before loading:
const script = useScript('/video.js', {
trigger: 'manual'
})
// Add warmup hint when likely needed
onVisible(videoContainer, () => {
script.warmup('preload')
})
// Load when definitely needed
onClick(videoContainer, () => {
script.load()
})
Best Practices
- Use composables to encapsulate script initialization
- Consider warmup strategies for performance
- Be mindful of proxy limitations
- Add error handling
- Use triggers to control loading timing
- Consider privacy implications of third-party scripts
Complete Example
const script = useScript({
src: 'https://example.com/api.js',
defer: true,
crossorigin: 'anonymous'
}, {
warmupStrategy: 'preconnect',
trigger: new Promise((resolve) => {
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
resolve(true)
observer.disconnect()
}
})
observer.observe(element)
})
})
script.onLoaded((api) => {
// Use API directly
api.initialize()
})
// Or use proxy for SSR-safe calls
script.proxy.initialize()