Skip to content

Commit 0faec39

Browse files
JoviDeCroockmarvinhagemeisterjacob-ebeydevelopitarmujahid
authored
feat: streaming rendering with Suspense boundaries as flush trigger (#296)
* Disable eslint lines-around-comment rule * Update test scripts to allow watch usage * Add streaming renderer * Switch to element nodes as markers instead * Switch away from global ids * Remove subtree option * feat: use comments instead of element as marker feat: use custom element for hydration feat: add onError to renderToChunks feat: add renderToPipeableStream * chore: use NodeIterator to locate comments This reduces code and *should* also be more performant than recursive JS iteration. See: https://developer.mozilla.org/en-US/docs/Web/API/NodeIterator * chore: remove redundancy and minify code * more minification * even more minification * Move files to new test dir structure * Fix linting error * fix ts types * fix Web Streams tests on Node <18 * Streaming renderer: factor chunking out of main entrypoint and rebase on #241 (#267) * use index.module.js when benchmarking, since its the output of `npm run transpile` * fix bench:v8 output path * update microbundle and turn off function inlining * fix JSX entrypoint and tests * fix type defintion to reflect removed exports and options * fix root copy of jsx types * optimize renderToString performance using switch and short-circuiting * Create bright-ligers-jam.md * Update bright-ligers-jam.md * Update bright-ligers-jam.md * Backport changes from #237 (child/parent properties, simplified Fragment handling) * ci: update github actions (#266) * ci: update actions/checkout to v3 * ci: update actions/cache to v3 * merge master * lockfile version * update benchmarking reference implmementation to 5.2.6 (6a0bec2) * fix tests * fix before diff hook being called on invalid vnodes * move non-exported files into a lib directory * update pretty implementation and move typedefs into a d.ts * Move chunked implementation out of the default entrypoint * update tests to reflect chunking being moved out of default entrypoint * fix d8 bench script --------- Co-authored-by: Abdul Rauf <[email protected]> * fix d8 bench path * try new way of getting mask as we are not setting it anymore * stop interfering with the real useId * show bug * partial fix * continuously fork promises * fix tests * fixes * update lockfiles * Create twelve-candles-walk.md * add build command * fix rebase issues * address comments * bump deps --------- Co-authored-by: Marvin Hagemeister <[email protected]> Co-authored-by: Jacob Ebey <[email protected]> Co-authored-by: Jason Miller <[email protected]> Co-authored-by: Jason Miller <[email protected]> Co-authored-by: Abdul Rauf <[email protected]>
1 parent f510fa5 commit 0faec39

30 files changed

+27971
-25847
lines changed

.changeset/grumpy-kings-flow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'preact-render-to-string': minor
3+
---
4+
5+
Introduce a streaming renderer which can be imported from `preact-render-to-string/stream` and `preact-render-to-string/stream-node`

.changeset/twelve-candles-walk.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"preact-render-to-string": patch
3+
---
4+
5+
streaming rendering with Suspense boundaries as flush trigger

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
/npm-debug.log
44
.DS_Store
55
/src/preact-render-to-string-tests.d.ts
6-
/benchmarks/.v8.mjs
6+
/benchmarks/.v8.modern.js
7+
/demo/node_modules

demo/index.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Vite App</title>
7+
</head>
8+
<body>
9+
<div id="app"><!--app-html--></div>
10+
<script type="module" src="/src/entry-client.jsx"></script>
11+
</body>
12+
</html>

demo/package-lock.json

Lines changed: 2165 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demo/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "demo",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "server.js",
6+
"type": "module",
7+
"scripts": {
8+
"dev": "vite --force",
9+
"build": "npm run build:client && npm run build:server",
10+
"build:client": "vite build --outDir dist/client",
11+
"build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server"
12+
},
13+
"author": "",
14+
"license": "ISC",
15+
"devDependencies": {
16+
"vite": "^4.1.4"
17+
},
18+
"dependencies": {
19+
"@preact/preset-vite": "^2.8.0",
20+
"express": "^4.18.2",
21+
"graphql": "^16.6.0",
22+
"preact": "^10.21.0",
23+
"urql": "latest"
24+
}
25+
}

demo/src/App.jsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { h } from 'preact';
2+
import { Suspense, lazy } from 'preact/compat';
3+
import { Client, Provider, cacheExchange, fetchExchange } from 'urql';
4+
5+
const client = new Client({
6+
url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
7+
exchanges: [cacheExchange, fetchExchange],
8+
suspense: true
9+
});
10+
11+
export function App({ head }) {
12+
const Pokemons = lazy(
13+
() =>
14+
new Promise((res) => {
15+
setTimeout(
16+
() => {
17+
res(import('./Pokemons.jsx'));
18+
},
19+
typeof document === 'undefined' ? 500 : 3000
20+
);
21+
})
22+
);
23+
return (
24+
<html>
25+
<head dangerouslySetInnerHTML={{ __html: head }} />
26+
<body>
27+
<Provider value={client}>
28+
<main>
29+
<h1>Our Counter application</h1>
30+
<Suspense fallback={<p>Loading...</p>}>
31+
<Pokemons />
32+
</Suspense>
33+
</main>
34+
{import.meta.env.DEV && (
35+
<script type="module" src="/src/entry-client.jsx" />
36+
)}
37+
</Provider>
38+
</body>
39+
</html>
40+
);
41+
}

demo/src/Pokemons.jsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { gql, useQuery } from 'urql';
2+
import { h } from 'preact';
3+
4+
const POKEMONS_QUERY = gql`
5+
query Pokemons($limit: Int!) {
6+
pokemons(limit: $limit) {
7+
id
8+
name
9+
}
10+
}
11+
`;
12+
13+
const Counter = () => {
14+
const [result] = useQuery({
15+
query: POKEMONS_QUERY,
16+
variables: { limit: 10 }
17+
});
18+
19+
const { data, fetching, error } = result;
20+
console.log('hydrated!');
21+
return (
22+
<div>
23+
{fetching && <p>Loading...</p>}
24+
25+
{error && <p>Oh no... {error.message}</p>}
26+
27+
{data && (
28+
<ul>
29+
{data.pokemons.map((pokemon) => (
30+
<li key={pokemon.id}>{pokemon.name}</li>
31+
))}
32+
</ul>
33+
)}
34+
</div>
35+
);
36+
};
37+
38+
export default Counter;

demo/src/entry-client.jsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { hydrate } from 'preact';
2+
import { App } from './App';
3+
4+
const config = { attributes: true, childList: true, subtree: true };
5+
const mut = new MutationObserver((mutationList, observer) => {
6+
for (const mutation of mutationList) {
7+
if (mutation.type === 'childList') {
8+
console.log('A child node has been added or removed.', mutation);
9+
} else if (mutation.type === 'attributes') {
10+
console.log(
11+
`The ${mutation.attributeName} attribute was modified.`,
12+
mutation
13+
);
14+
}
15+
}
16+
});
17+
mut.observe(document, config);
18+
19+
hydrate(<App />, document);

demo/src/entry-server.jsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { renderToPipeableStream } from '../../src/stream-node';
2+
import { App } from './App';
3+
4+
export function render({ res, head }) {
5+
res.socket.on('error', (error) => {
6+
console.error('Fatal', error);
7+
});
8+
const { pipe, abort } = renderToPipeableStream(<App head={head} />, {
9+
onShellReady() {
10+
res.statusCode = 200;
11+
res.setHeader('Content-type', 'text/html');
12+
pipe(res);
13+
},
14+
onErrorShell(error) {
15+
res.statusCode = 500;
16+
res.send(
17+
`<!doctype html><p>An error ocurred:</p><pre>${error.message}</pre>`
18+
);
19+
}
20+
});
21+
22+
// Abandon and switch to client rendering if enough time passes.
23+
// Try lowering this to see the client recover.
24+
setTimeout(abort, 20000);
25+
}

0 commit comments

Comments
 (0)