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:
- Recursively scans
.next/static/for JavaScript files - Finds files containing
TURBOPACK_WORKER_LOCATION - Replaces the assignment with an empty string
- 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
Leave a Reply