Skip to content

Commit

Permalink
Add BatchingRemoteConnection helper for batching changes to a polyf…
Browse files Browse the repository at this point in the history
…illed DOM (#394)
  • Loading branch information
lemonmade authored Jul 11, 2024
1 parent 9e138c6 commit 22e6512
Showing 3 changed files with 83 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/silly-students-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@remote-dom/core': minor
---

Add `BatchingRemoteConnection` helper for batching changes to a polyfilled DOM
19 changes: 19 additions & 0 deletions packages/core/README.md
Original file line number Diff line number Diff line change
@@ -324,6 +324,25 @@ const root = document.createElement('remote-root');
root.connect(connection);
```

#### `BatchingRemoteConnection`

The `RemoteConnection` object you receive from `RemoteReceiver.connection` is a simple object that immediately communicates all updates to the host environment. When using `RemoteMutationObserver`, documented above, this is not a major issue, since the `MutationObserver` API automatically batches DOM mutations. However, it can be more of a problem when using Remote DOM in a web worker (typically, with the `RemoteRootElement` wrapper), where no such batching is performed.

To improve performance in these cases, you can use the `BatchingRemoteConnection` class, which batches updates from the remote environment that happen in the same JavaScript task. This class is a subclass of `RemoteConnection`, and can be used directly in place of the original connection object:

```ts
import {
BatchingRemoteConnection,
RemoteRootElement,
} from '@remote-dom/core/elements';

customElements.define('remote-root', RemoteRootElement);

const root = document.createElement('remote-root');

root.connect(new BatchingRemoteConnection(connection));
```

#### `RemoteFragmentElement`

Some APIs in [`@remote-dom/preact`](../preact) and [`@remote-dom/react`](../react) need to create an HTML element as a generic container. This element is not defined by default, so if you use these features, you must define a matching custom element for this container. Remote DOM calls this element `remote-fragment`, and you can define this element using the `RemoteFragmentElement` constructor:
59 changes: 59 additions & 0 deletions packages/core/source/elements/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type {RemoteConnection, RemoteMutationRecord} from '../types.ts';

/**
* A wrapper around a `RemoteConnection` that batches mutations. By default, this
* all calls are flushed in a queued microtask, but this can be customized by passing
* a custom `batch` option.
*/
export class BatchingRemoteConnection {
readonly #connection: RemoteConnection;
#queued: RemoteMutationRecord[] | undefined;
#batch: (action: () => void) => void;

constructor(
connection: RemoteConnection,
{
batch = createDefaultBatchFunction(),
}: {batch?: (action: () => void) => void} = {},
) {
this.#connection = connection;
this.#batch = batch;
}

mutate(records: any[]) {
let queued = this.#queued;

if (queued) {
queued.push(...records);
return;
}

queued = [...records];
this.#queued = queued;

this.#batch(() => {
this.#connection.mutate(queued);
this.#queued = undefined;
});
}
}

function createDefaultBatchFunction() {
let channel: MessageChannel;

return (queue: () => void) => {
// In environments without a `MessageChannel`, use a `setTimeout` fallback.
if (typeof MessageChannel !== 'function') {
setTimeout(() => {
queue();
}, 0);
}

// `MessageChannel` trick that forces the code to run on the next task.
channel ??= new MessageChannel();
channel.port1.onmessage = () => {
queue();
};
channel.port2.postMessage(null);
};
}

0 comments on commit 22e6512

Please sign in to comment.