Resolving WASM Module Loading Errors in Next.js v16 Turbopack + Vercel

Tadashi Shigeoka ·  Thu, December 11, 2025

After upgrading to Next.js v16 and migrating the bundler from Webpack to Turbopack, you may encounter unexpected errors in functionality that previously worked correctly.

Particular attention is needed when using WebAssembly (WASM) modules. This article explains in detail the problem where WASM modules from @embedpdf/pdfium, a PDF processing library, could no longer be loaded, along with the solution.

Background: Errors After Turbopack Migration

In Giselle, a project we are developing, we were using the @embedpdf/pdfium package for functionality that extracts text from PDFs and registers it in a Vector Store. It worked without issues in the Webpack environment, but after switching to Turbopack, the following error occurred.

Error: Cannot find module '@embedpdf/pdfium/pdfium.wasm'

This error occurred both in the local development environment (next dev --turbo) and after deployment to Vercel.

The actual fix is documented in feat(api): Fix pdfium.wasm path resolution with Turbopack · Pull Request #2460 · giselles-ai/giselle.

Below, I’ll explain the cause of the problem and the solution in detail.

Root Cause of the Problem

Through investigation, I found that this problem was caused by two interrelated factors.

1. Module Resolution Failure Due to Turbopack’s Static Analysis

The problematic code was using require.resolve to resolve the path to the WASM file.

// Problematic code
import { createRequire } from "node:module";
 
const requireBaseUrl = new URL(".", import.meta.url);
const moduleRequire = createRequire(requireBaseUrl);
 
// Executed at module top-level, making it subject to static analysis at build time
const PDFIUM_WASM_PATH = moduleRequire.resolve("@embedpdf/pdfium/pdfium.wasm");

While Webpack can process this code without issues, Turbopack attempts to statically analyze createRequire().resolve() at build time. However, since Turbopack doesn’t yet fully support dynamic evaluation of some Node.js APIs, it couldn’t correctly interpret this dynamic path resolution, resulting in build errors or runtime errors when resolving the WASM file path.

2. Different File Structures in Vercel Serverless Environment

The location of node_modules differs between the local development environment and Vercel’s serverless function environment.

  • Local: Can directly reference node_modules in the project root.
  • Vercel: Through the build process, dependencies are placed in special paths like .next/server/node_modules/.

Due to this difference, a single static path resolution logic couldn’t handle both environments.

Solution: Combining Three Approaches

To solve these problems, I implemented a robust solution combining three approaches.

Solution 1: Lazy Path Resolution

First, to avoid Turbopack’s build-time static analysis, I moved the WASM file path resolution from the module’s top-level into a function. This way, path resolution is not executed until the WASM is actually needed, avoiding build-time issues.

// Before
// const PDFIUM_WASM_PATH = moduleRequire.resolve("@embedpdf/pdfium/pdfium.wasm");
 
// After
let cachedWasmPath: string | null = null;
 
function getPdfiumWasmPath(): string {
  if (cachedWasmPath !== null) {
    // Cache and reuse the resolved path
    return cachedWasmPath;
  }
  // Path resolution logic (described below)
  // ...
}

Solution 2: Multi-Path Fallback Strategy

Next, to absorb the environmental differences between local and Vercel, I introduced a fallback strategy that searches multiple candidate paths in order.

It checks for file existence with fs.existsSync() and uses the first valid path found.

import { createRequire } from "node:module";
import { join } from "node:path";
import { existsSync } from "node:fs";
 
function getPdfiumWasmPath(): string {
  if (cachedWasmPath !== null) {
    return cachedWasmPath;
  }
 
  const requireBaseUrl = new URL(".", import.meta.url);
  const searchedPaths: string[] = [];
 
  // Define multiple path resolution functions
  const possiblePathFinders = [
    // 1. Standard Node.js module resolution (mainly for local development)
    () => {
      const moduleRequire = createRequire(requireBaseUrl);
      return moduleRequire.resolve("@embedpdf/pdfium/pdfium.wasm");
    },
    // 2. Relative path from process.cwd() (for Vercel)
    () => join(process.cwd(), "node_modules/@embedpdf/pdfium/dist/pdfium.wasm"),
    // 3. Relative path from __dirname (fallback)
    () => join(__dirname, "../node_modules/@embedpdf/pdfium/dist/pdfium.wasm"),
    // 4. Support for Vercel serverless function's special structure
    () => join(process.cwd(), ".next/server/node_modules/@embedpdf/pdfium/dist/pdfium.wasm"),
  ];
 
  for (const findPath of possiblePathFinders) {
    try {
      const path = findPath();
      searchedPaths.push(path);
      if (existsSync(path)) {
        cachedWasmPath = path; // Cache the found path
        return path;
      }
    } catch (e) {
      // Ignore if resolve fails and continue to next
    }
  }
 
  // If not found in any path
  throw new Error(
    `Could not find pdfium.wasm. Searched paths:\n${searchedPaths.map((p) => `  - ${p}`).join("\n")}`
  );
}

Solution 3: Using the wasmBinary Option

Previously, I was using the locateFile callback to tell PDFium the location of the WASM file, but this method tends to depend on bundler behavior.

So I changed to read the WASM file binary directly with fs.readFileSync() and pass it via the wasmBinary option to initPdfium. This completely bypasses the bundler’s module resolution system and provides more direct control over WASM module initialization.

import { readFileSync } from "node:fs";
import initPdfium from "@embedpdf/pdfium";
 
// ... getPdfiumWasmPath() implementation ...
 
// Before
/*
pendingModule = initPdfium({
  locateFile: (fileName: string, prefix: string) => {
    if (fileName === "pdfium.wasm") {
      return PDFIUM_WASM_PATH; // Return dynamically resolved path
    }
    return prefix + fileName;
  },
});
*/
 
// After
const wasmBinary = readFileSync(getPdfiumWasmPath());
pendingModule = initPdfium({
  wasmBinary, // Pass WASM binary data directly
});

Adding next.config.ts Configuration

With the fixes so far, WASM loading succeeds, but when deploying to Vercel, the WASM file needs to be included in the serverless function bundle.

Using outputFileTracingIncludes in next.config.ts, you can explicitly include specific files in the bundle.

// next.config.ts
 
const pdfiumWasmInclude = "node_modules/@embedpdf/pdfium/dist/pdfium.wasm";
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // ...other settings
  experimental: {
    outputFileTracingIncludes: {
      "/api/vector-stores/document/[documentVectorStoreId]/documents": [
        pdfiumWasmInclude,
      ],
      "/api/vector-stores/cron/document/ingest": [pdfiumWasmInclude],
    },
  },
};
 
export default nextConfig;

Summary: Challenges and Solutions

Here’s a summary of the approaches taken:

ChallengeSolution
Turbopack statically analyzes createRequire().resolve() at build timeLazy evaluation (path resolution inside function) to avoid build-time analysis
File structure differs between Vercel and local environmentsFallback strategy searching multiple paths in order
locateFile callback depends on bundlerPass binary directly via wasmBinary option
WASM file not found in deployment environmentInclude in bundle with outputFileTracingIncludes
Difficult to debugInclude all searched paths in error message

Turbopack is a very powerful tool, but there are still areas under development. When dealing with external binary files like WASM, implementation that considers bundler behavior is required.

When facing similar issues, the following approaches introduced in this article should be helpful:

  • Lazy path resolution
  • Multi-path fallback
  • wasmBinary option

That’s all from the Gemba on resolving the “Cannot find module ‘xxx.wasm’” error in Next.js v16 + Turbopack + Vercel environments.

References