Skip to content

Commit 6848928

Browse files
committed
PHP: Replace Worker Threads with Web Workers (breaking) (#471)
Iframe worker threads were introduced as a workaround for limitations in web browsers. Namely: * Chrome crashed when using WASM in web workers * Firefox didn't support ESM workers at all Both problems are now solved: * WordPress/wordpress-playground#1 * mdn/content#26774 There are no more reasons to keep maintaining the iframe worker thread backend. Let's remove it and lean fully on web workers. This commit changes the signature of `spawnPHPWorkerThread` from `spawnPHPWorkerThread(workerUrl, workerType, options)` to `spawnPHPWorkerThread(workerUrl, options)` and is therefore breaking.
1 parent 7ec0a46 commit 6848928

File tree

12 files changed

+16
-152
lines changed

12 files changed

+16
-152
lines changed

packages/docs/site/docs/11-architecture/08-browser-concepts.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ The [`@php-wasm/web`](https://github.com/WordPress/wordpress-playground/blob/tru
1818

1919
- [**Browser tab orchestrates everything**](./09-browser-tab-orchestrates-execution.md) – The browser tab is the main program. Closing or reloading it means destroying the entire execution environment.
2020
- [**Iframe-based rendering**](./10-browser-iframe-rendering.md) – Every response produced by the PHP server must be rendered in an iframe to avoid reloading the browser tab when the user clicks on a link.
21-
- [**PHP Worker Thread**](./11-browser-php-worker-threads.md) – The PHP server is slow and must run in a worker thread, otherwise handling requests freezes the website UI.
21+
- [**PHP Worker Thread**](./11-browser-php-worker-threads.md) – The PHP server is slow and must run in a web worker, otherwise handling requests freezes the website UI.
2222
- [**Service Worker routing**](./12-browser-service-workers.md) – All HTTP requests originating in that iframe must be intercepted by a Service worker and passed on to the PHP worker thread for rendering.

packages/docs/site/docs/11-architecture/09-browser-tab-orchestrates-execution.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,14 @@ Here's what that boot sequence looks like in code:
2424
**/app.ts**:
2525

2626
```ts
27-
import { consumeAPI, PHPClient, recommendedWorkerBackend, registerServiceWorker, spawnPHPWorkerThread } from '@php-wasm/web';
27+
import { consumeAPI, PHPClient, registerServiceWorker, spawnPHPWorkerThread } from '@php-wasm/web';
2828

2929
const workerUrl = '/worker-thread.js';
3030

3131
export async function startApp() {
3232
const phpClient = consumeAPI<PlaygroundWorkerEndpoint>(
3333
await spawnPHPWorkerThread(
3434
workerUrl, // Valid Worker script URL
35-
recommendedWorkerBackend, // "webworker" or "iframe", see the docstring
3635
{
3736
wpVersion: 'latest',
3837
phpVersion: '7.4', // Startup options
Lines changed: 7 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PHP Worker Threads
22

3-
PHP is always ran in a separate thread we'll call a "Worker Thread." This happens to ensure the PHP runtime doesn't slow down the website.
3+
PHP is always ran in a [web worker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) to ensure the PHP runtime doesn't slow down the user interface of the main website.
44

55
Imagine the following code:
66

@@ -11,81 +11,22 @@ Imagine the following code:
1111

1212
As soon as you click that button the browser will freeze and you won't be able to type in the input. That's just how browsers work. Whether it's a for loop or a PHP server, running intensive tasks slows down the user interface.
1313

14-
### Initiating the worker thread
14+
### Initiating web workers
1515

16-
Worker threads are separate programs that can process heavy tasks outside of the main application. They must be initiated by the main JavaScript program living in the browser tab. Here's how:
16+
Web workers are separate programs that can process heavy tasks outside of the main application. They must be initiated by the main JavaScript program living in the browser tab. Here's how:
1717

1818
```ts
1919
const phpClient = consumeAPI<PHPClient>(
2020
spawnPHPWorkerThread(
21-
'/worker-thread.js', // Valid Worker script URL
22-
recommendedWorkerBackend // "webworker" or "iframe", see the docstring
21+
'/worker-thread.js' // Valid Worker script URL
2322
)
2423
);
2524
await phpClient.isReady();
2625
await phpClient.run({ code: `<?php echo "Hello from the thread!";` });
2726
```
2827

29-
Worker threads can use any multiprocessing technique like an iframe, WebWorker, or a SharedWorker (not implemented). See the next sections to learn more about the supported backends.
28+
### Controlling web workers
3029

31-
### Controlling the worker thread
30+
Exchanging messages is the only way to control web workers. The main application has no access to functions or variables inside of a web workeer. It can only send and receive messages using `worker.postMessage` and `worker.onmessage = function(msg) { }`.
3231

33-
The main application controls the worker thread by sending and receiving messages. This is implemented via a backend-specific flavor of `postMessage` and `addEventListener('message', fn)`.
34-
35-
Exchanging messages is the only way to control the worker threads. Remember – it is separate programs. The main app cannot access any functions or variables defined inside of the worker thread.
36-
37-
Conveniently, [consumeAPI](/api/web/function/consumeAPI) returns an easy-to-use API object that exposes specific worker thread features and handles the message exchange internally.
38-
39-
### Worker thread backends
40-
41-
Worker threads can use any multiprocessing technique like an iframe, WebWorker, or a SharedWorker. This package provides two backends out of the box:
42-
43-
#### `webworker`
44-
45-
Spins a new `Worker` instance with the given Worker Thread script. This is the classic solution for multiprocessing in the browser and it almost became the only, non-configurable backend. The `iframe` backend is handy to work around webworkers limitations in the browsers. For example, [Firefox does not support module workers](https://github.com/mdn/content/issues/24402) and [WASM used to crash webworkers in Chrome](https://github.com/WordPress/wordpress-playground/issues/1).
46-
47-
Example usage:
48-
49-
```ts
50-
const phpClient = consumeAPI<PHPClient>(spawnPHPWorkerThread('/worker-thread.js', 'webworker'));
51-
```
52-
53-
#### `iframe`
54-
55-
Loads the PHPRequestHandler in a new iframe to avoid crashes in browsers based on Google Chrome.
56-
57-
The browser will **typically** run an iframe in a separate thread in one of the two cases:
58-
59-
1. The `iframe-worker.html` is served with the `Origin-Agent-Cluster: ?1` header. If you're running the Apache webserver, this package ships a `.htaccess` that will add the header for you.
60-
2. The `iframe-worker.html` is served from a different origin. For convenience, you could point a second domain name to the same server and directory and use it just for the `iframe-worker.html`.
61-
62-
Pick your favorite option and make sure to use it for serving the `iframe-worker.html`.
63-
64-
Example usage:
65-
66-
**/app.js**:
67-
68-
```ts
69-
const phpClient = consumeAPI<PHPClient>(spawnPHPWorkerThread('/iframe-worker.html?script=/worker-thread.js', 'iframe'));
70-
```
71-
72-
**/iframe-worker.html** (Also provided in `@php-wasm/web` package):
73-
74-
```js
75-
<!DOCTYPE html>
76-
<html>
77-
<head></head>
78-
<body style="padding: 0; margin: 0">
79-
<script>
80-
const script = document.createElement('script');
81-
script.type = 'module';
82-
script.src = getEscapeScriptName();
83-
document.body.appendChild(script);
84-
85-
function getEscapeScriptName() {
86-
// Grab ?script= query parameter and securely escape it
87-
}
88-
</script>
89-
</body>
90-
</html>
91-
```
32+
This can be tedious, which is why Playground provides a convenient [consumeAPI](/api/web/function/consumeAPI) function that abstracts the message exchange and exposes specific functions from the web worker. This is why we can call `phpClient.run` in the example above.

packages/php-wasm/web-service-worker/src/initialize-service-worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export async function convertFetchEventToPHPRequest(event: FetchEvent) {
160160
* what this function uses to broadcast the message.
161161
*
162162
* @param message The message to broadcast.
163-
* @param scope Target worker thread scope.
163+
* @param scope Target web worker scope.
164164
* @returns The request ID to receive the reply.
165165
*/
166166
export async function broadcastMessageExpectReply(message: any, scope: string) {
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
11
AddType application/wasm .wasm
2-
3-
<FilesMatch "iframe-worker.html$">
4-
Header set Origin-Agent-Cluster: ?1
5-
</FilesMatch>

packages/php-wasm/web/src/lib/index.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,4 @@ export { getPHPLoaderModule } from './get-php-loader-module';
88
export { registerServiceWorker } from './register-service-worker';
99

1010
export { parseWorkerStartupOptions } from './worker-thread/parse-startup-options';
11-
export {
12-
spawnPHPWorkerThread,
13-
recommendedWorkerBackend,
14-
} from './worker-thread/spawn-php-worker-thread';
11+
export { spawnPHPWorkerThread } from './worker-thread/spawn-php-worker-thread';

packages/php-wasm/web/src/lib/register-service-worker.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export async function registerServiceWorker<
3131
// the update:
3232
await registration.update();
3333

34-
// Proxy the service worker messages to the worker thread:
34+
// Proxy the service worker messages to the web worker:
3535
navigator.serviceWorker.addEventListener(
3636
'message',
3737
async function onMessage(event) {
Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,16 @@
1-
/**
2-
* Recommended Worker Thread backend.
3-
* It's typically "webworker", but in Firefox it's "iframe"
4-
* because Firefox doesn't support module workers with dynamic imports.
5-
* See https://github.com/mdn/content/issues/24402
6-
*/
7-
export const recommendedWorkerBackend = (function () {
8-
const isFirefox =
9-
typeof navigator !== 'undefined' &&
10-
navigator?.userAgent?.toLowerCase().indexOf('firefox') > -1;
11-
if (isFirefox) {
12-
return 'iframe';
13-
} else {
14-
return 'webworker';
15-
}
16-
})();
17-
181
/**
192
* Spawns a new Worker Thread.
203
*
214
* @param workerUrl The absolute URL of the worker script.
22-
* @param workerBackend The Worker Thread backend to use. Either 'webworker' or 'iframe'.
235
* @param config
246
* @returns The spawned Worker Thread.
257
*/
268
export async function spawnPHPWorkerThread(
279
workerUrl: string,
28-
workerBackend: 'webworker' | 'iframe' = 'webworker',
2910
startupOptions: Record<string, string> = {}
3011
) {
3112
workerUrl = addQueryParams(workerUrl, startupOptions);
32-
33-
if (workerBackend === 'webworker') {
34-
return new Worker(workerUrl, { type: 'module' });
35-
} else if (workerBackend === 'iframe') {
36-
return (await createIframe(workerUrl)).contentWindow!;
37-
} else {
38-
throw new Error(`Unknown backendName: ${workerBackend}`);
39-
}
13+
return new Worker(workerUrl, { type: 'module' });
4014
}
4115

4216
function addQueryParams(
@@ -52,15 +26,3 @@ function addQueryParams(
5226
}
5327
return urlWithOptions.toString();
5428
}
55-
56-
async function createIframe(workerDocumentURL: string) {
57-
const iframe = document.createElement('iframe');
58-
const relativeUrl = '/' + workerDocumentURL.split('/').slice(-1)[0];
59-
iframe.src = relativeUrl;
60-
iframe.style.display = 'none';
61-
document.body.appendChild(iframe);
62-
await new Promise((resolve) => {
63-
iframe.addEventListener('load', resolve);
64-
});
65-
return iframe;
66-
}

packages/playground/remote/iframe-worker.html

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
AddType application/wasm .wasm
22
AddType application/octet-stream .data
33

4-
<FilesMatch "iframe-worker.html$">
5-
Header set Origin-Agent-Cluster: ?1
6-
</FilesMatch>
7-
84
Header set Cross-Origin-Resource-Policy: cross-origin
95
Header set Cross-Origin-Embedder-Policy: credentialless

0 commit comments

Comments
 (0)