This early demo runs a full WordPress directly in the browser without a PHP server! While it isn’t fully stable yet, it is a major breakthrough that could transform learning, contributing, and using WordPress. This post explores the opportunities and explains in detail how it works.
Your help is needed to realize the vision laid out below. None of the presented mockups and early explorations are currently reliably implemented. This project needs volunteers to stabilize the code and build revolutionary tools on top of it. If you’d like to be a part of it, please say so in the comments!
Learning WordPress in the browser
The code examples in the WordPress handbook could become runeditable, like in this early preview:
Furthermore, an in-browser IDE could lead new contributors through solving their first “Good first issue” without setting up a local development environment. Just like this early preview:
Finally, a guided code editor could become a primary teaching tool for new developers. They would click a “Build your first block” button on WordPress.org and immediately start coding. This is what it could look like:
WordPress Developer tools
Testing code on different WordPress, PHP, and Gutenberg versions currently require a tedious setup. With an in-browser WordPress IDE, it wouldn’t require any setup at all. As a developer, you would switch between the different versions by selecting different entries in a box:
Taking it further, the continuous integration pipeline could replay the failed tests right in the browser and provide a code editor to debug and fix the problem on the spot:
On a different note, the desktop and mobile apps could reuse WordPress code by running an actual WordPress instance – even when offline.
Finally, WordPress could potentially be scaled up by spinning up many tiny self-contained WASM instances directly on the edge servers.
Showcasing
Embedding a demo of your plugin, pattern, or theme directly on the website makes another great use case. One such demo lives on wpreadme.com, an in-browser WordPress readme.txt editor. Here’s what the plugin directory could look like:
Today, a WordPress server is required, and ideally, one WordPress per user to always present the same initial state.
The in-browser WordPress enables one private WordPress per user with no marginal server cost. Everyone can start logged-in as an admin with no security risks.
Furthermore, importing an existing WordPress website into WASM runtime would create a staging website. Users could try themes and plugins on a copy of their site without affecting the live sites. Then, once the staging website looks good, they’d click a button and publish the changes.
How else would you use the in-browser WordPress? Please share your ideas in the comments so more use cases can be considered in the future.
How does the in-browser WordPress work?
The code lives in the GitHub repository.
In short:
- PHP is compiled to WASM with Emscripten
- WordPress is packaged into a data bundle
- A service worker traps HTTP requests and re-routes them to WordPress.
See more details below.
PHP is compiled to WASM with Emscripten
Firstly, PHP is compiled using an adjusted recipe from the php-wasm repo. It’s powered by Emscripten, a drop-in replacement for the C compiler. Unfortunately, MySQL currently cannot run as WASM. However, SQLite can, and WordPress supports SQLite via the wp-db-sqlite plugin.
Emscripten compilation yields two files: webworker-php.wasm
, which is the assembly, and webworker-php.js
, which downloads the assembly file, creates a virtual heap, and exposes named native functions conveniently wrapped to accept and return JavaScript data types.
You need to load the webworker-php.js
in a webworker and add a tiny wrapper class:
12345 | const php = new PHPWrapper(); console.log( await php.run(`<?php echo "Hello world" ;`).stdout ); // "Hello world" |
See the Dockerfile, the compilation script, and the php-wasm playground.
WordPress is packaged into a data bundle
WebAssembly PHP runtime has its own filesystem and WordPress is shoehorned into it as a data bundle.
First, a fresh WordPress distribution is downloaded and patched with the wp-db-sqlite plugin. It’s about 66MB large, but an optimization pipeline makes that 46MB by minifying the PHP files and removing non-essential static assets. Getting down to just 12MB is possible, but it’s not easy.
Second, the WordPress installation process kicks in. A script serves our WordPress via the built-in PHP server and sends a special curl request to /wp-admin/install.php?step=2
. Unfortunately, wp-cli is hard-wired to MySQL and not a good match here.
Lastly, Emscripten’s file_packager turns our WordPress copy into a data file named wp.data
and a JavaScript loader named wp.js. Loading both the PHP runtime from webworker-php.js and the WordPress data bundle from wp.js allows you to run <?php require “wordpress/index.php”;
right in the browser!
A service worker reroutes the HTTP to WordPress
WordPress reads request information from $_SERVER, $_GET, $_COOKIE and so on. Normally these variables are populated by Apache, Nginx, or another web server. However, in this case, there isn’t one.
Instead, the superglobal variables are populated “manually”:
123456789 | function request(path, method, body, cookies) { return php.run(`<?php $_POST = json_parse( '${JSON.stringify(body)}' ); $_COOKIE = json_parse( '${JSON.stringify(cookies)}' ); $_SERVER [ 'REQUEST_URI' ] = $path ; $_SERVER [ 'REQUEST_METHOD' ] = "${method}" ; require "wordpress/index.php" ; `); } |
That takes care of the request, but capturing the complete HTTP response is still necessary. The PHP outputs information in only two ways: stdout and stderr. The response body comes out via stdout, but the HTTP status code and headers don’t. They need to be manually streamed to stderr where the JavaScript app can capture them:
123456 | register_shutdown_function( function () use () { $stdErr = fopen ( 'php://stderr' , 'w' ); fwrite( $stdErr , json_encode([ 'status_code' , http_response_code()]) . "\n" ); fwrite( $stdErr , json_encode([ 'session_id' , session_id()]) . "\n" ); fwrite( $stdErr , json_encode([ 'headers' , headers_list()]) . "\n" ); } |
The final request handler is similar to this:
123456789101112131415161718 | function request(path, method, body, cookies) { const { stdout, stderr, exitCode } = php.run(`<?php $_POST = json_parse( '${JSON.stringify(body)}' ); // ... Populate other superglobals... register_shutdown_function( function () use () { $stdErr = fopen ( 'php://stderr' , 'w' ); fwrite( $stdErr , json_encode([ 'status_code' , http_response_code()]) . "\n" ); // Output other information } require "wordpress/index.php" ; `); const { statusCode, headers, body } = rawOutputToResponse({ stdout, stderr, exitCode }); return { statusCode, headers, body }; } |
The actual code is much more involved, but it’s based on the same idea.
Node.js is supported, too
Running WordPress in different JS runtimes is a matter of connecting these major building blocks with runtime-specific plumbing. Today there is an in-browser version and a node.js version. Here’s how they differ:
In-browser version
- Emscripten compiles PHP using the ENVIRONMENT=worker configuration flag.
- WordPress is split into .php files required by the backend and frontend-only static assets. The backend is minified and bundled into a single .data file.
- A web worker loads PHP WebAssembly, downloads WordPress, mounts it in PHP’s in-memory filesystem, and registers. Webworker is needed to avoid freezing the UI while handling requests.
- A service worker traps the browser’s HTTP requests and re-routes the .php ones to the web worker.
WordPress is rendered in an iframe by a minimal index.html once both workers are loaded.
Node.js version
- Emscripten compiles PHP for the Node.js runtime.
- A Node.js script loads the WebAssembly PHP and mounts a local WordPress directory inside the PHP filesystem.
- The same Node.js script starts a web server. It serves most requests as static files, except the .php ones, which it routes to WordPress.
WordPress is served on localhost:8888.
Running an in-browser WordPress IDE with Stackblitz
Stackblitz is the in-browser IDE that makes Svelte docs interactive:
It could do the same for WordPress docs; see this early preview.
Stackblitz doesn’t need a specialized backend. It runs Node.js, npm, and webpack right in the browser via WebAssembly. Other than hosting and delivering a few megabytes of data, Stackblitz has no marginal costs. This idea is called Web Containers.
Learning WordPress and writing code used to be separated. Now they can be one and the same. From runnable code snippets to new, svelte-like docs formats, WebContainers + WebAssembly WordPress is an educational game-changer.
WordPress runs in Stackblitz via a Node.js server
The minimal WordPress plugin dev setup on Stackblitz consists of:
- A node.js WordPress server (via an npm package)
- A plugin directory mounted inside of mu-plugins
- A wp-scripts setup to watch and bundle the JavaScript files
@gziolo went a step further and prepared a version with an editable Gutenberg block generated via create-block. All it took was running the npx @wordpress/create-block example-static
command as shown in the create-block documentation, and restarting the dev server with npm run start:
However, a Node.js WordPress server on Stackblitz is sluggish
Rendering a single wp-admin page takes a second locally but up to 40 seconds on Stackblitz. That’s much longer than most developers are willing to wait. Here’s why that happens:
A local Node.js WordPress server works like this:
- WordPress runs on WebAssembly PHP
- WebAssembly PHP runs on a native Node.js
However, a Stackblitz Node.js WordPress server has an extra layer:
- WordPress runs on WebAssembly PHP
- WebAssembly PHP runs on WebAssembly Node.js
- WebAssembly Node.js runs on a native Chrome
It’s like a box in a box: A WebAssembly runtime in Chrome runs Node.js that has its own WebAssembly runtime. WordPress runs on the latter, not on the former, and that indirection causes a massive slowdown.
Luckily, the browser can run WebAssembly natively without an intermediate Node.js layer.
An in-browser WASM WordPress is really fast
Here’s a Stackblitz WordPress setup using the webworker WordPress version:
It is much faster! Unfortunately, the speed comes at the expense of simplicity.
Stackblitz requires a service worker just like the in-browser WASM WordPress. However, you can only have one per domain. Therefore, the WordPress files in this example are hosted on a Netlify domain.
Then, the Stackblitz files can’t be directly mounted into the in-browser PHP filesystem. The changes need to be synchronized manually. In this example, a node.js file watcher notifies the PHP webworker about the updates via WebSockets and onmessage
/postMessage
. There are bugs, but all are fixable.
Known issues
WASM WordPress issues:
- The in-browser WordPress builds sometimes crash the Chrome tab, see the GitHub issue. Firefox, Safari, and node.js are much more stable.
Stackblitz integration issues:
- The in-browser Stackblitz setup doesn’t watch subdirectories.
- WordPress exceeds Stackblitz size limits unless imported as a node_module.
- Stackblitz corrupts a predefined binary .sqlite database file. A base64-encoded version is shipped instead.
- Stackblitz doesn’t pass a server-side set-cookie header to the browser. That’s a bug. Workaround: the express.js server handles the cookies internally.