Yoga Layout Engine in Cloudflare Workers in 2025

3 min readby Patrick Miller

Running Facebook's Yoga layout engine in Cloudflare Workers for server-side layout calculations and OpenGraph image generation at the edge.

I would like to run @vercel/og or similar to generate OpenGraph images in Cloudflare Workers. @vercel/og uses satori to convert a layout to svg. And satori uses yoga for the internal layout calculations. However, getting the latest yoga to run in Cloudflare Workers is still tricky.

The Problem: Workers WASM Constraints

Cloudflare Workers has strict requirements for WebAssembly instantiation. Unlike other runtimes, Workers doesn't support compiling WASM from arbitrary data blobs. You must import it as a module:

import yogaModule from './yoga.wasm' // WebAssembly.Module

For comparison, you could produce a WebAssembly.Module in Node.js with WebAssembly.compile(fs.readFile()) and browsers with WebAssembly.compileStreaming(fetch()).

On the positive side, the @cloudflare/vite-plugin, as of 2025, also supports Cloudflare's special syntax by importing via either .wasm or .wasm?module. (There was a time where it did not support this and it was even harder to use wasm in Cloudflare Vite projects.)

Historical Solutions

Yoga 3.0 introduced inline base64 WASM bundling, but this format is incompatible with Workers. Previously, I used shuding/yoga-wasm-web, which was designed for web environments and worked well in Workers. Unfortunately, it hasn't been updated since 2023.

The ecosystem is fragmented. Even Satori (Vercel's SVG generator) now uses an inline patched version of yoga-layout instead of yoga-wasm-web. Many Yoga forks get abandoned over time, making long-term maintenance challenging.

Technical Deep Dive

Yoga is built with emscripten, and the config flag causing Workers incompatibility is SINGLE_FILE=1, which inlines the WASM binary in the JavaScript wrapper.

To make Yoga Workers-compatible, you would also need to override emscripten's instantiateWasm() function. The yoga-wasm-web fork demonstrates this pattern:

import wrapAsm from './yoga/javascript/src_js/wrapAsm.js'
import yoga from './tmp/yoga.mjs'

export default async function (wasm: WebAssembly.Module) {
    const mod = await yoga({
        instantiateWasm(info, receive) {
            WebAssembly.instantiate(wasm, info).then((instance) => {
                if (instance instanceof WebAssembly.Instance) {
                    receive(instance)
                } else {
                    receive(instance.instance)
                }
            })
        },
    })
    return wrapAsm(mod)
}

The abandoned worker-emscripten-template suggests a similar approach:

import yogaModule from './yoga'

let emscripten_module = new Promise((resolve, reject) => {
    emscripten({
        instantiateWasm(info, receive) {
            let instance = new WebAssembly.Instance(yogaModule, info)
            receive(instance)
            return instance.exports
        },
    }).then((module) => {
        resolve({
            init: module.cwrap('init', 'number', ['number']),
            resize: module.cwrap('resize', 'number', ['number', 'number']),
            module: module,
        })
    })
})

Current State & Options

Creating a Workers-compatible Yoga build requires two changes:

  1. Remove the SINGLE_FILE=1 flag to generate separate WASM files
  2. Create a custom wrapper providing instantiateWasm

The yoga-wasm-web fork has a clean design. It's a lightweight repo with the main yoga as a submodule, overriding compilation and providing platform-specific wrappers. However, maintaining a personal fork just for this use case seems tedious.

Conclusion

While Yoga in Cloudflare Workers is technically feasible, the ecosystem fragmentation makes it challenging to maintain. Cloudflare has a non-standard way of loading wasm, and emscripten yoga and satori all seem not particularly interested in supporting this approach.