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

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *