OpenCV 5 | WebAssembly | C++ | SIMD | pthreads
Building OpenCV 5 as a Static C++ WebAssembly Library with Emscripten
By Antal Zsiros | June 9, 2026 | Antal.AI Blog
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:\emsdkC:\Projects\opencv-5.0.0-sourceC:\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.
7. Link your own C++ project against the OpenCV 5 WASM build
When OpenCV was built with pthreads and SIMD, your final application must also be compiled and linked with compatible flags. In practice, that means using -pthread and -msimd128 in your own project too.
Manual em++ example
em++ main.cpp Engine.cpp CvUtils.cpp NeuralNetwork.cpp Detector.cpp -o app.js ^
-I "C:\Projects\opencv-5.0.0-wasm-simd-pthread\include" ^
-L "C:\Projects\opencv-5.0.0-wasm-simd-pthread\lib" ^
-pthread -msimd128 ^
-lopencv_world -llibjpeg-turbo -llibopenjp2 -llibpng -lzlib -llibprotobuf ^
-lembind ^
-sPTHREAD_POOL_SIZE=4 ^
-sALLOW_MEMORY_GROWTH=1 ^
-sNO_EXIT_RUNTIME=1 ^
-sASSERTIONS=1 ^
-s "EXPORTED_RUNTIME_METHODS=['ccall','cwrap']" ^
--preload-file resources.bin
CMake example
In CMake, keep compile options and link options separate. This avoids warnings such as linker flag ignored during compilation: '--bind' and -lembind: 'linker' input unused.
cmake_minimum_required(VERSION 3.20)
project(MyOpenCVWasmApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(demo
Engine/web_main.cpp
Engine/SingleImageProcessorEngine.cpp
Engine/EverythingDetector.cpp
)
target_include_directories(demo PRIVATE
"C:/Projects/3rdParty/opencv-5.0.0-wasm-simd-pthread/include"
)
target_link_directories(demo PRIVATE
"C:/Projects/3rdParty/opencv-5.0.0-wasm-simd-pthread/lib"
)
target_compile_options(demo PRIVATE
-pthread
-msimd128
)
target_link_libraries(demo PRIVATE
opencv_world
libjpeg-turbo
libopenjp2
libpng
zlib
libprotobuf
)
target_link_options(demo PRIVATE
-pthread
-msimd128
-lembind
-sPTHREAD_POOL_SIZE=4
-sALLOW_MEMORY_GROWTH=1
-sNO_EXIT_RUNTIME=1
-sASSERTIONS=1
"SHELL:-s EXPORTED_RUNTIME_METHODS=['ccall','cwrap']"
"SHELL:--preload-file resources.bin"
)
If you do not use Embind, remove -lembind. If you use Embind, add it only at link time, not while compiling individual .cpp files.
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:
| Check | Expected value | Why it matters |
| WebAssembly SIMD | -msimd128 in C and C++ flags | Enables the WebAssembly SIMD target and LLVM autovectorization. |
| OpenCV intrinsics | CV_ENABLE_INTRINSICS=ON | Lets OpenCV use vectorized code paths where available. |
| Parallel backend | Parallel framework: pthreads | Critical for avoiding large DNN regressions on heavier networks. |
| Application link flags | -pthread -msimd128 -sPTHREAD_POOL_SIZE=4 | The 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.