OpenCV 5 | WebAssembly | C++ | SIMD | pthreads

Building OpenCV 5 as a Static C++ WebAssembly Library with Emscripten

This is a practical, end-to-end guide for compiling OpenCV 5 into static WebAssembly-ready C++ libraries, then linking those libraries into your own C++ application that runs in the browser. The goal is not to use the OpenCV.js JavaScript API, but to keep using normal C++ OpenCV code and compile the whole application to WebAssembly.

Want to skip the OpenCV build step?

You can download my already compiled OpenCV 5 WebAssembly package here. The package is intended for C++ projects compiled with Emscripten, SIMD and pthreads enabled.

Download precompiled OpenCV 5 WASM package

1. What this build is for

This guide is for developers who already have a C++ codebase that uses OpenCV directly and want to compile that full C++ project to WebAssembly using Emscripten.

The output of the OpenCV build is a set of static libraries, most importantly libopencv_world.a, plus the required third-party static libraries such as protobuf, zlib, libpng, JPEG and JPEG 2000. Your own project will later link against those static libraries using em++ or CMake with the Emscripten toolchain.

Important: This is not an OpenCV.js tutorial. OpenCV.js is useful when you want a JavaScript-facing OpenCV API. Here, the goal is to keep writing C++ and only expose your own API to JavaScript if needed.

The configuration below enables both WebAssembly SIMD and pthreads. For DNN inference this matters a lot: in my tests, a build that had SIMD but no pthread-based parallel backend was much slower than my previous OpenCV 4.13 build. After enabling pthreads again, the DNN performance returned to the expected range.

2. Prerequisites

The commands below are written for Windows PowerShell, because that is the environment used while preparing this guide. The same approach also works on Linux and macOS with small path and shell syntax changes.

Required tools

  • Git
  • Python 3.8 or newer
  • CMake
  • Ninja
  • Emscripten SDK
  • OpenCV 5 source code

Recommended folders

  • C:\emsdk
  • C:\Projects\opencv-5.0.0-source
  • C:\Projects\opencv-5.0.0-wasm-simd-pthread

Install CMake and Ninja before configuring OpenCV. Make sure both tools are available on your PATH:

cmake --version
ninja --version

3. Install and initialize Emscripten

Emscripten is the compiler toolchain that turns your C++ project into WebAssembly. The official Emscripten SDK is managed through emsdk. I usually place it directly under C:\emsdk.

cd C:\
git clone https://github.com/emscripten-core/emsdk.git
cd C:\emsdk
.\emsdk install latest
.\emsdk activate latest
.\emsdk_env.ps1

If you use the classic Windows Command Prompt instead of PowerShell, use emsdk.bat and emsdk_env.bat instead:

cd C:\emsdk
emsdk.bat install latest
emsdk.bat activate latest
emsdk_env.bat

Verify that Emscripten is active in the current terminal:

emcc --version
em++ --version
emcmake --version

You need to run the Emscripten environment script in every new terminal session unless you made the activation permanent. If em++ or emcmake is not found, re-run C:\emsdk\emsdk_env.ps1.

4. Download OpenCV 5 source code

The OpenCV source code can be downloaded from the official GitHub releases page:

https://github.com/opencv/opencv/releases

Download the OpenCV 5 source archive and extract it to a folder such as:

C:\Projects\opencv-5.0.0-source

Alternatively, if you want the current 5.x branch directly from Git, you can clone it:

cd C:\Projects\3rdParty
git clone --branch 5.x --depth 1 https://github.com/opencv/opencv.git opencv-5.0.0-source

OpenCV 5 is new and may still change. Keep your OpenCV 4.13 build around as a known-good fallback until you have benchmarked your actual models and workloads with OpenCV 5.

5. Configure OpenCV 5 for static C++ WebAssembly

Start from a clean build directory. This avoids stale CMake cache values from older 4.x or non-pthread builds.

cd C:\Projects\opencv-5.0.0-source
Remove-Item -Recurse -Force cmake-build-wasm-simd-pthread -ErrorAction SilentlyContinue
mkdir cmake-build-wasm-simd-pthread
cd cmake-build-wasm-simd-pthread

Recommended configuration: SIMD + pthreads

This is the configuration I recommend for DNN-heavy workloads. It produces static OpenCV libraries, enables WebAssembly SIMD with -msimd128, and keeps OpenCV's parallel backend active through pthreads.

emcmake cmake -S .. -B . -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX="C:\Projects\opencv-5.0.0-wasm-simd-pthread" -DCMAKE_CXX_STANDARD=17 -DCMAKE_C_FLAGS="-pthread -msimd128" -DCMAKE_CXX_FLAGS="-pthread -msimd128" -DCMAKE_EXE_LINKER_FLAGS="-pthread" -DCPU_BASELINE= -DCPU_DISPATCH= -DCV_ENABLE_INTRINSICS=ON -DWITH_PTHREADS_PF=ON -DBUILD_SHARED_LIBS=OFF -DBUILD_opencv_world=ON -DBUILD_LIST=core,imgproc,imgcodecs,dnn,features,flann,geometry,calib,stereo,ptcloud,objdetect,video -DBUILD_ZLIB=ON -DBUILD_PROTOBUF=ON -DBUILD_IPP_IW=OFF -DBUILD_ITT=OFF -DBUILD_JPEG=OFF -DBUILD_PACKAGE=OFF -DBUILD_PERF_TESTS=OFF -DBUILD_TESTS=OFF -DBUILD_EXAMPLES=OFF -DBUILD_opencv_apps=OFF -DBUILD_opencv_gapi=OFF -DBUILD_opencv_highgui=OFF -DBUILD_opencv_ml=OFF -DBUILD_opencv_photo=OFF -DBUILD_opencv_stitching=OFF -DBUILD_opencv_videoio=OFF -DBUILD_opencv_ts=OFF -DBUILD_opencv_js=OFF -DBUILD_opencv_java=OFF -DBUILD_opencv_python=OFF -DBUILD_opencv_python3=OFF -DWITH_1394=OFF -DWITH_ADE=OFF -DWITH_FFMPEG=OFF -DWITH_GSTREAMER=OFF -DWITH_GTK=OFF -DWITH_IPP=OFF -DWITH_ITT=OFF -DWITH_JASPER=OFF -DWITH_LAPACK=OFF -DWITH_OPENCL=OFF -DWITH_OPENCLAMDBLAS=OFF -DWITH_OPENCLAMDFFT=OFF -DOPENCV_DNN_OPENCL=OFF -DWITH_PNG=ON -DWITH_PROTOBUF=ON -DWITH_TIFF=OFF -DWITH_WEBP=OFF -DWITH_QUIRC=OFF

Same command, formatted for PowerShell

emcmake cmake -S .. -B . -G Ninja `
  -DCMAKE_BUILD_TYPE=Release `
  -DCMAKE_INSTALL_PREFIX="C:\Projects\opencv-5.0.0-wasm-simd-pthread" `
  -DCMAKE_CXX_STANDARD=17 `
  -DCMAKE_C_FLAGS="-pthread -msimd128" `
  -DCMAKE_CXX_FLAGS="-pthread -msimd128" `
  -DCMAKE_EXE_LINKER_FLAGS="-pthread" `
  -DCPU_BASELINE= `
  -DCPU_DISPATCH= `
  -DCV_ENABLE_INTRINSICS=ON `
  -DWITH_PTHREADS_PF=ON `
  -DBUILD_SHARED_LIBS=OFF `
  -DBUILD_opencv_world=ON `
  -DBUILD_LIST=core,imgproc,imgcodecs,dnn,features,flann,geometry,calib,stereo,ptcloud,objdetect,video `
  -DBUILD_ZLIB=ON `
  -DBUILD_PROTOBUF=ON `
  -DBUILD_IPP_IW=OFF `
  -DBUILD_ITT=OFF `
  -DBUILD_JPEG=OFF `
  -DBUILD_PACKAGE=OFF `
  -DBUILD_PERF_TESTS=OFF `
  -DBUILD_TESTS=OFF `
  -DBUILD_EXAMPLES=OFF `
  -DBUILD_opencv_apps=OFF `
  -DBUILD_opencv_gapi=OFF `
  -DBUILD_opencv_highgui=OFF `
  -DBUILD_opencv_ml=OFF `
  -DBUILD_opencv_photo=OFF `
  -DBUILD_opencv_stitching=OFF `
  -DBUILD_opencv_videoio=OFF `
  -DBUILD_opencv_ts=OFF `
  -DBUILD_opencv_js=OFF `
  -DBUILD_opencv_java=OFF `
  -DBUILD_opencv_python=OFF `
  -DBUILD_opencv_python3=OFF `
  -DWITH_1394=OFF `
  -DWITH_ADE=OFF `
  -DWITH_FFMPEG=OFF `
  -DWITH_GSTREAMER=OFF `
  -DWITH_GTK=OFF `
  -DWITH_IPP=OFF `
  -DWITH_ITT=OFF `
  -DWITH_JASPER=OFF `
  -DWITH_LAPACK=OFF `
  -DWITH_OPENCL=OFF `
  -DWITH_OPENCLAMDBLAS=OFF `
  -DWITH_OPENCLAMDFFT=OFF `
  -DOPENCV_DNN_OPENCL=OFF `
  -DWITH_PNG=ON `
  -DWITH_PROTOBUF=ON `
  -DWITH_TIFF=OFF `
  -DWITH_WEBP=OFF `
  -DWITH_QUIRC=OFF

Why CPU_BASELINE= and CPU_DISPATCH= are empty: WebAssembly is not an x86 binary target. We do not want OpenCV to try to force SSE, AVX or other native CPU dispatch paths during the CMake checks. WebAssembly SIMD is enabled separately with -msimd128.

Do not write -pthread-msimd128-march=native. That is one invalid, glued-together compiler flag. The correct form is -pthread -msimd128, with spaces.

6. Build and install OpenCV

After CMake configuration succeeds, build and install the libraries:

ninja
ninja install

If you want more diagnostic output, especially when the build appears to be stuck, use:

ninja -v -j1

What to check in the configure log

Before you spend time linking your own application, check that the OpenCV configure summary contains these lines or their equivalents:

Built as dynamic libs?:      NO
C++ standard:                17
C++ Compiler:                C:/emsdk/upstream/emscripten/em++.bat
C++ flags (Release):         -pthread -msimd128 ... -O3 -DNDEBUG
Parallel framework:          pthreads
Install to:                  C:/Projects/3rdParty/opencv-5.0.0-wasm-simd-pthread

Double-check the install path. Accidentally installing OpenCV 5 into an old OpenCV 4.13 folder is an easy way to create confusing link and runtime problems.

8. Browser deployment requirements

A pthread-enabled WebAssembly build uses SharedArrayBuffer. Modern browsers require cross-origin isolation headers before they allow that feature.

Your server should send at least these headers for the page and the generated assets:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

For same-origin static files, this additional header is often useful too:

Cross-Origin-Resource-Policy: same-origin

Minimal local test server with COOP/COEP headers

from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler

class Handler(SimpleHTTPRequestHandler):
    def end_headers(self):
        self.send_header("Cross-Origin-Opener-Policy", "same-origin")
        self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
        self.send_header("Cross-Origin-Resource-Policy", "same-origin")
        super().end_headers()

ThreadingHTTPServer(("localhost", 8000), Handler).serve_forever()

Save this as serve_coop_coep.py in the folder where your generated .html, .js, .wasm and optional .data files are located, then run:

python serve_coop_coep.py

Open the app from:

http://localhost:8000/

Opening the generated HTML file directly from the filesystem is not a valid test for pthread-enabled WebAssembly. Serve it through HTTP with the required headers.

9. Notes about OpenCV 5 DNN

OpenCV 5 introduces major DNN changes. A new inference engine coexists with the classic one, and DNN loading functions such as cv::dnn::readNet() and cv::dnn::readNetFromONNX() have an engine selection parameter with ENGINE_AUTO as the default.

For performance-sensitive projects, benchmark your real model with both the default engine and the classic engine where possible. OpenCV 5 can also be built with ONNX Runtime integration, but the lean WebAssembly build above intentionally leaves ONNX Runtime: NO. That does not mean OpenCV DNN cannot load ONNX models; it only means the external ONNX Runtime backend is not linked into this build.

For browser-based DNN inference, the most important build-level performance checks are:

CheckExpected valueWhy it matters
WebAssembly SIMD-msimd128 in C and C++ flagsEnables the WebAssembly SIMD target and LLVM autovectorization.
OpenCV intrinsicsCV_ENABLE_INTRINSICS=ONLets OpenCV use vectorized code paths where available.
Parallel backendParallel framework: pthreadsCritical for avoiding large DNN regressions on heavier networks.
Application link flags-pthread -msimd128 -sPTHREAD_POOL_SIZE=4The final app must match the OpenCV library build mode.

10. Troubleshooting

CMake error: compiler does not support baseline optimization flags

This usually means OpenCV tried to configure native CPU dispatch flags such as SSE or AVX for a WebAssembly target, or your compiler flags were malformed.

Use empty OpenCV CPU baseline and dispatch settings:

-DCPU_BASELINE= -DCPU_DISPATCH=

Also make sure your flags are separated by spaces:

-DCMAKE_CXX_FLAGS="-pthread -msimd128"

Not like this:

-DCMAKE_CXX_FLAGS="-pthread-msimd128-march=native"

DNN is much slower than an older OpenCV 4.x build

First check whether your configure log says:

Parallel framework: none

If so, you built OpenCV without the pthread parallel backend. Rebuild OpenCV with:

-DCMAKE_C_FLAGS="-pthread -msimd128" -DCMAKE_CXX_FLAGS="-pthread -msimd128" -DWITH_PTHREADS_PF=ON

Then also link your final application with -pthread, -msimd128 and a thread pool size such as -sPTHREAD_POOL_SIZE=4.

Does -march=native matter for WebAssembly?

For native x86 builds, -march=native lets the compiler target the exact CPU in your build machine. For WebAssembly, the relevant portable SIMD switch is -msimd128. Do not rely on -march=native for browser performance.

Build appears to hang at final linking

OpenCV 5 with opencv_world, DNN, protobuf, pthreads and Embind can take a long time to link. Use verbose single-job mode to see the exact command:

ninja -v -j1

If you see warnings like linker flag ignored during compilation: '--bind' or -lembind: 'linker' input unused, move linker-only flags from target_compile_options() to target_link_options().

Generated app fails in the browser with pthread errors

Make sure the server sends COOP and COEP headers. A pthread-enabled Emscripten build cannot transparently fall back to single-threaded mode. If you need to support browsers or hosts without cross-origin isolation, build a separate non-pthread variant and select the correct package at runtime.

11. Reference links