Front-end Engineering Lab

Resource Hints

Use preconnect, dns-prefetch, and preload to eliminate network latency and accelerate resource loading.

Resource Hints

Problem

Each external resource (APIs, CDNs, fonts) requires DNS lookup, TCP connection, and TLS handshake. This adds 300-1000ms of latency before downloading even starts. For apps using multiple third-party services, this compounds quickly.

Solution

Use resource hints to tell the browser to perform these operations early, in parallel with page parsing.

/**
 * Resource hint types
 */
type HintType = 'preconnect' | 'dns-prefetch' | 'preload' | 'prefetch' | 'prerender';

interface ResourceHint {
  type: HintType;
  href: string;
  as?: string;
  crossOrigin?: 'anonymous' | 'use-credentials';
}

/**
 * Add resource hint to document
 */
function addResourceHint(hint: ResourceHint): HTMLLinkElement {
  const link = document.createElement('link');
  link.rel = hint.type;
  link.href = hint.href;

  if (hint.as) {
    link.as = hint.as;
  }

  if (hint.crossOrigin) {
    link.crossOrigin = hint.crossOrigin;
  }

  document.head.appendChild(link);
  return link;
}

/**
 * Resource hint manager
 */
class ResourceHintManager {
  private hints = new Set<string>();

  /**
   * Preconnect to origin (DNS + TCP + TLS)
   */
  public preconnect(origin: string, crossOrigin?: 'anonymous' | 'use-credentials'): void {
    const key = `preconnect:${origin}`;
    
    if (this.hints.has(key)) {
      return;
    }

    addResourceHint({
      type: 'preconnect',
      href: origin,
      crossOrigin,
    });

    this.hints.add(key);
  }

  /**
   * DNS prefetch only (lighter than preconnect)
   */
  public dnsPrefetch(origin: string): void {
    const key = `dns-prefetch:${origin}`;
    
    if (this.hints.has(key)) {
      return;
    }

    addResourceHint({
      type: 'dns-prefetch',
      href: origin,
    });

    this.hints.add(key);
  }

  /**
   * Preload critical resource
   */
  public preload(
    href: string,
    as: string,
    crossOrigin?: 'anonymous' | 'use-credentials'
  ): void {
    const key = `preload:${href}`;
    
    if (this.hints.has(key)) {
      return;
    }

    addResourceHint({
      type: 'preload',
      href,
      as,
      crossOrigin,
    });

    this.hints.add(key);
  }

  /**
   * Prefetch resource for next navigation
   */
  public prefetch(href: string): void {
    const key = `prefetch:${href}`;
    
    if (this.hints.has(key)) {
      return;
    }

    addResourceHint({
      type: 'prefetch',
      href,
    });

    this.hints.add(key);
  }

  /**
   * Preconnect to multiple origins
   */
  public preconnectMultiple(origins: string[]): void {
    // Browsers limit preconnect to 6-10 origins
    const maxPreconnects = 6;
    origins.slice(0, maxPreconnects).forEach((origin) => {
      this.preconnect(origin);
    });
  }
}

// Practical examples

/**
 * Setup resource hints for common third-party services
 */
class ThirdPartyOptimizer {
  private hintManager = new ResourceHintManager();

  public setupAnalytics(): void {
    // Google Analytics
    this.hintManager.preconnect('https://www.google-analytics.com');
    this.hintManager.preconnect('https://www.googletagmanager.com');
  }

  public setupFonts(): void {
    // Google Fonts
    this.hintManager.preconnect('https://fonts.googleapis.com');
    this.hintManager.preconnect('https://fonts.gstatic.com', 'anonymous');
    
    // Preload critical font
    this.hintManager.preload(
      'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Me5WZLCzYlKw.woff2',
      'font',
      'anonymous'
    );
  }

  public setupCDN(): void {
    // Cloudflare CDN
    this.hintManager.preconnect('https://cdnjs.cloudflare.com');
  }

  public setupAPIs(apiOrigins: string[]): void {
    // Preconnect to primary API
    this.hintManager.preconnect(apiOrigins[0], 'use-credentials');
    
    // DNS prefetch for secondary APIs
    apiOrigins.slice(1).forEach((origin) => {
      this.hintManager.dnsPrefetch(origin);
    });
  }

  public setupImages(): void {
    // Image CDN
    this.hintManager.preconnect('https://images.example.com');
    
    // Preload hero image
    this.hintManager.preload(
      'https://images.example.com/hero.jpg',
      'image'
    );
  }
}

/**
 * Smart prefetching based on user behavior
 */
class SmartPrefetcher {
  private hintManager = new ResourceHintManager();
  private hoveredLinks = new Map<string, number>();

  constructor() {
    this.setupLinkHoverPrefetch();
  }

  /**
   * Prefetch when user hovers over link
   */
  private setupLinkHoverPrefetch(): void {
    let hoverTimeout: ReturnType<typeof setTimeout> | null = null;

    document.addEventListener('mouseover', (event: MouseEvent) => {
      const target = event.target as HTMLElement;
      const link = target.closest('a[href]') as HTMLAnchorElement;

      if (!link || !link.href) {
        return;
      }

      // Wait 200ms to avoid prefetching during fast mouse movement
      hoverTimeout = setTimeout(() => {
        this.prefetchLink(link.href);
      }, 200);
    });

    document.addEventListener('mouseout', () => {
      if (hoverTimeout) {
        clearTimeout(hoverTimeout);
      }
    });
  }

  /**
   * Prefetch link intelligently
   */
  private prefetchLink(href: string): void {
    // Don't prefetch same origin (already connected)
    const url = new URL(href, window.location.origin);
    
    if (url.origin === window.location.origin) {
      this.hintManager.prefetch(href);
    } else {
      this.hintManager.preconnect(url.origin);
    }

    // Track hover count
    const count = this.hoveredLinks.get(href) || 0;
    this.hoveredLinks.set(href, count + 1);
  }

  /**
   * Prefetch most likely next page
   */
  public prefetchNextPage(likelyUrls: string[]): void {
    // Prefetch top 3 most likely next pages
    likelyUrls.slice(0, 3).forEach((url) => {
      this.hintManager.prefetch(url);
    });
  }
}

/**
 * Conditional preconnect based on feature usage
 */
class ConditionalPreconnect {
  private hintManager = new ResourceHintManager();

  public setupVideoPlayer(): void {
    // Only preconnect to video CDN if video element exists
    if (document.querySelector('video')) {
      this.hintManager.preconnect('https://video-cdn.example.com');
    }
  }

  public setupPayment(): void {
    // Preconnect to payment gateway when user adds to cart
    document.addEventListener('cart:add', () => {
      this.hintManager.preconnect('https://api.stripe.com');
    });
  }

  public setupChat(): void {
    // Preconnect to chat service on user interaction
    const chatButton = document.getElementById('chat-button');
    
    chatButton?.addEventListener('click', () => {
      this.hintManager.preconnect('https://chat.example.com');
    }, { once: true });
  }
}

HTML Implementation

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Optimized App</title>
  
  <!-- Preconnect to critical origins (highest priority) -->
  <link rel="preconnect" href="https://api.example.com" crossorigin="use-credentials">
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous">
  
  <!-- DNS prefetch for secondary origins (lower priority) -->
  <link rel="dns-prefetch" href="https://www.google-analytics.com">
  <link rel="dns-prefetch" href="https://cdn.example.com">
  
  <!-- Preload critical resources -->
  <link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin="anonymous">
  <link rel="preload" href="/images/hero.jpg" as="image">
  <link rel="preload" href="/styles/critical.css" as="style">
  
  <link rel="stylesheet" href="/styles/critical.css">
</head>
<body>
  <img src="/images/hero.jpg" alt="Hero">
</body>
</html>

Performance Impact

Before Resource Hints

DNS lookup:    120ms
TCP connect:   80ms
TLS handshake: 100ms
Total:         300ms (before first byte)

After Resource Hints

DNS lookup:    0ms (done in parallel)
TCP connect:   0ms (done in parallel)
TLS handshake: 0ms (done in parallel)
Total:         0ms (ready when needed)

Time saved: 300-1000ms per origin

When to Use Each Hint

Preconnect (DNS + TCP + TLS)

Use for:

  • Primary API origin
  • Critical font CDN
  • Payment gateway
  • Auth service

Don't use for:

  • More than 6 origins (browser limit)
  • Unlikely-to-be-used resources
  • Same-origin resources (already connected)

DNS Prefetch (DNS only)

Use for:

  • Secondary origins
  • Analytics services
  • Social media widgets
  • When preconnect limit reached

Preload (high priority download)

Use for:

  • Critical fonts (above-the-fold)
  • Hero images (LCP)
  • Critical CSS
  • Main JavaScript bundle

Don't use for:

  • Below-the-fold resources
  • More than 3-5 resources (causes congestion)
  • Resources that might not be used

Prefetch (low priority for next page)

Use for:

  • Next page in a flow
  • Likely navigation targets
  • User-hovered links

Best Practices

  1. Limit preconnects: Maximum 6 origins
  2. Prioritize: Use preconnect for immediate needs, dns-prefetch for future
  3. Combine with CDN: Fewer origins = fewer preconnects needed
  4. Test on real networks: 3G/4G shows biggest gains
  5. Monitor: Track connection timing in analytics

Impact on Core Web Vitals

  • LCP: 15-30% faster (fonts and images load sooner)
  • FID: Minimal direct impact
  • CLS: Prevents layout shift from late-loading fonts

For apps with millions of users, resource hints can save hundreds of hours of cumulative waiting time per day.

Usage

// Initialize optimizers
const thirdParty = new ThirdPartyOptimizer();
thirdParty.setupFonts();
thirdParty.setupAnalytics();
thirdParty.setupAPIs(['https://api.example.com']);

// Smart prefetching
const prefetcher = new SmartPrefetcher();
prefetcher.prefetchNextPage(['/checkout', '/cart']);

// Conditional preconnect
const conditional = new ConditionalPreconnect();
conditional.setupVideoPlayer();
conditional.setupPayment();

Resource hints are free performance wins with minimal code and zero runtime cost.

On this page