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 blocking
    • defer - Execute in order after load
    • fetchpriority="low" - Prioritize critical resources
  • Added privacy attributes:
    • crossorigin="anonymous" - Prevent cookie access
    • referrerpolicy="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 loading
  • preconnect/dns-prefetch - Use for cross-origin scripts loading within 10s
  • false - 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

  1. Use composables to encapsulate script initialization
  2. Consider warmup strategies for performance
  3. Be mindful of proxy limitations
  4. Add error handling
  5. Use triggers to control loading timing
  6. 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()
Did this page help you?