Tag: Next.js

  • Fixing Next.js 16 + Turbopack + assetPrefix + Web Worker Loading Issue

    When using latest Next.js 16 with Turbopack and a custom assetPrefix (like a CDN), Web Workers fail to load with an invalid URL error.

    What Turbopack generates:

    self.TURBOPACK_WORKER_LOCATION = "https://example.com";
    self.TURBOPACK_NEXT_CHUNK_URLS = [
      "https://cdn.example.com/_next/static/chunks/turbopack-ba07cdf40de9152f.js",
      "https://cdn.example.com/_next/static/chunks/8569516e2fd59227.js",
    ];
    importScripts(...self.TURBOPACK_NEXT_CHUNK_URLS.map(c => self.TURBOPACK_WORKER_LOCATION + c).reverse());

    Turbopack is clever here to use technique like jantimon/remote-web-worker to load the external script via importScripts. But the issue is that TURBOPACK_WORKER_LOCATION is being concatenated with full URLs from TURBOPACK_NEXT_CHUNK_URLS, which already contain the assetPrefix.

    The Solution

    A simple post-build script that patches the generated worker files:

    Create scripts/fix-turbopack-workers.ts:

    #!/usr/bin/env bun
    /**
     * Fix Turbopack worker URL issue when using assetPrefix
     *
     * Turbopack generates worker code that incorrectly concatenates full URLs:
     * self.TURBOPACK_WORKER_LOCATION + "https://cdn.example.com/..."
     *
     * This script patches worker files to set TURBOPACK_WORKER_LOCATION to empty string.
     */
    
    import { readdir, readFile, writeFile } from 'node:fs/promises'
    import { join } from 'node:path'
    
    const NEXT_DIR = '.next'
    const STATIC_DIR = join(NEXT_DIR, 'static')
    
    async function findAndFixWorkerFiles(dir: string): Promise<void> {
      try {
        const entries = await readdir(dir, { withFileTypes: true })
    
        for (const entry of entries) {
          const fullPath = join(dir, entry.name)
    
          if (entry.isDirectory()) {
            await findAndFixWorkerFiles(fullPath)
          } else if (entry.isFile() && entry.name.endsWith('.js')) {
            await fixWorkerFile(fullPath)
          }
        }
      } catch {
        // Ignore errors (e.g., directory doesn't exist)
      }
    }
    
    async function fixWorkerFile(filePath: string): Promise<void> {
      try {
        const content = await readFile(filePath, 'utf-8')
    
        // Check if this is a Turbopack worker file
        if (!content.includes('TURBOPACK_WORKER_LOCATION')) {
          return
        }
    
        // Replace TURBOPACK_WORKER_LOCATION assignment with empty string
        const fixedContent = content.replace(
          /self\.TURBOPACK_WORKER_LOCATION\s*=\s*[^;]+;/g,
          'self.TURBOPACK_WORKER_LOCATION = "";'
        )
    
        if (content !== fixedContent) {
          await writeFile(filePath, fixedContent, 'utf-8')
          console.log(`✓ Fixed worker file: ${filePath}`)
        }
      } catch (error) {
        console.error(`Error processing ${filePath}:`, error)
      }
    }
    
    async function main() {
      console.log('Fixing Turbopack worker files...')
      await findAndFixWorkerFiles(STATIC_DIR)
      console.log('Done!')
    }
    
    main()
    

    Update package.json:

    {
      "scripts": {
        "build": "next build --turbopack && bun scripts/fix-turbopack-workers.ts"
      }
    }

    How It Works

    The script:

    1. Recursively scans .next/static/ for JavaScript files
    2. Finds files containing TURBOPACK_WORKER_LOCATION
    3. Replaces the assignment with an empty string
    4. Since the chunk URLs already contain the full CDN path, concatenating with an empty string works perfectly as a workaround

    After the fix:

    self.TURBOPACK_WORKER_LOCATION = "";
    // Now this works: "" + "https://cdn.example.com/_next/..."

    Why Not Use Webpack?

    Webpack works fine with the assetPrefix, but:

    • Next.js 16’s Turbopack is significantly faster
    • We want to use the latest build system
    • This simple post-build script is a clean, minimal fix. And Webpack still need remote-web-worker patch to work