Skip to content

Commit

Permalink
feat: streaming rendering with Suspense boundaries as flush trigger (#…
Browse files Browse the repository at this point in the history
…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 <abdulraufmujahid@gmail.com>

* 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 <hello@marvinh.dev>
Co-authored-by: Jacob Ebey <jacob.ebey@live.com>
Co-authored-by: Jason Miller <jason@developit.ca>
Co-authored-by: Jason Miller <developit@users.noreply.github.com>
Co-authored-by: Abdul Rauf <abdulraufmujahid@gmail.com>
  • Loading branch information
6 people committed May 6, 2024
1 parent f510fa5 commit 0faec39
Show file tree
Hide file tree
Showing 30 changed files with 27,971 additions and 25,847 deletions.
5 changes: 5 additions & 0 deletions .changeset/grumpy-kings-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'preact-render-to-string': minor
---

Introduce a streaming renderer which can be imported from `preact-render-to-string/stream` and `preact-render-to-string/stream-node`
5 changes: 5 additions & 0 deletions .changeset/twelve-candles-walk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"preact-render-to-string": patch
---

streaming rendering with Suspense boundaries as flush trigger
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
/npm-debug.log
.DS_Store
/src/preact-render-to-string-tests.d.ts
/benchmarks/.v8.mjs
/benchmarks/.v8.modern.js
/demo/node_modules
12 changes: 12 additions & 0 deletions demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/entry-client.jsx"></script>
</body>
</html>
2,165 changes: 2,165 additions & 0 deletions demo/package-lock.json

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "demo",
"version": "1.0.0",
"description": "",
"main": "server.js",
"type": "module",
"scripts": {
"dev": "vite --force",
"build": "npm run build:client && npm run build:server",
"build:client": "vite build --outDir dist/client",
"build:server": "vite build --ssr src/entry-server.jsx --outDir dist/server"
},
"author": "",
"license": "ISC",
"devDependencies": {
"vite": "^4.1.4"
},
"dependencies": {
"@preact/preset-vite": "^2.8.0",
"express": "^4.18.2",
"graphql": "^16.6.0",
"preact": "^10.21.0",
"urql": "latest"
}
}
41 changes: 41 additions & 0 deletions demo/src/App.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { h } from 'preact';
import { Suspense, lazy } from 'preact/compat';
import { Client, Provider, cacheExchange, fetchExchange } from 'urql';

const client = new Client({
url: 'https://trygql.formidable.dev/graphql/basic-pokedex',
exchanges: [cacheExchange, fetchExchange],
suspense: true
});

export function App({ head }) {
const Pokemons = lazy(
() =>
new Promise((res) => {
setTimeout(
() => {
res(import('./Pokemons.jsx'));
},
typeof document === 'undefined' ? 500 : 3000
);
})
);
return (
<html>
<head dangerouslySetInnerHTML={{ __html: head }} />
<body>
<Provider value={client}>
<main>
<h1>Our Counter application</h1>
<Suspense fallback={<p>Loading...</p>}>
<Pokemons />
</Suspense>
</main>
{import.meta.env.DEV && (
<script type="module" src="/src/entry-client.jsx" />
)}
</Provider>
</body>
</html>
);
}
38 changes: 38 additions & 0 deletions demo/src/Pokemons.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { gql, useQuery } from 'urql';
import { h } from 'preact';

const POKEMONS_QUERY = gql`
query Pokemons($limit: Int!) {
pokemons(limit: $limit) {
id
name
}
}
`;

const Counter = () => {
const [result] = useQuery({
query: POKEMONS_QUERY,
variables: { limit: 10 }
});

const { data, fetching, error } = result;
console.log('hydrated!');
return (
<div>
{fetching && <p>Loading...</p>}

{error && <p>Oh no... {error.message}</p>}

{data && (
<ul>
{data.pokemons.map((pokemon) => (
<li key={pokemon.id}>{pokemon.name}</li>
))}
</ul>
)}
</div>
);
};

export default Counter;
19 changes: 19 additions & 0 deletions demo/src/entry-client.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { hydrate } from 'preact';
import { App } from './App';

const config = { attributes: true, childList: true, subtree: true };
const mut = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.', mutation);
} else if (mutation.type === 'attributes') {
console.log(
`The ${mutation.attributeName} attribute was modified.`,
mutation
);
}
}
});
mut.observe(document, config);

hydrate(<App />, document);
25 changes: 25 additions & 0 deletions demo/src/entry-server.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { renderToPipeableStream } from '../../src/stream-node';
import { App } from './App';

export function render({ res, head }) {
res.socket.on('error', (error) => {
console.error('Fatal', error);
});
const { pipe, abort } = renderToPipeableStream(<App head={head} />, {
onShellReady() {
res.statusCode = 200;
res.setHeader('Content-type', 'text/html');
pipe(res);
},
onErrorShell(error) {
res.statusCode = 500;
res.send(
`<!doctype html><p>An error ocurred:</p><pre>${error.message}</pre>`
);
}
});

// Abandon and switch to client rendering if enough time passes.
// Try lowering this to see the client recover.
setTimeout(abort, 20000);
}
60 changes: 60 additions & 0 deletions demo/vite.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import path from 'path';
import { promises as fs } from 'fs';

function ssrPlugin() {
return {
name: 'ssrPlugin',

configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (req.url !== '/') {
return next();
}

const { render } = await server.ssrLoadModule(
path.resolve(__dirname, './src/entry-server')
);

// setTimeout(abort, 10000);

const indexHtml = await fs.readFile(
path.resolve(__dirname, './index.html'),
'utf-8'
);

const url = new URL('http://localhost:5173/' + req.url);
const template = await server.transformIndexHtml(
url.toString(),
indexHtml
);

const head = template.match(/<head>(.+?)<\/head>/s)[1];

return render({ res, head });
});
}
};
}

export default defineConfig({
// @ts-ignore
ssr: {
noExternal: /./
},
build: {
ssrManifest: true,
commonjsOptions: {
transformMixedEsModules: true
}
},
resolve: {
alias: {
preact: path.resolve(__dirname, './node_modules/preact'),
'preact/compat': path.resolve(__dirname, './node_modules/preact/compat'),
'preact/hooks': path.resolve(__dirname, './node_modules/preact/hooks')
}
},
plugins: [ssrPlugin(), preact()]
});
2 changes: 1 addition & 1 deletion jsx.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface Options {
skipFalseAttributes?: boolean;
}

export default function renderToStringPretty(
export default function render(
vnode: VNode,
context?: any,
options?: Options
Expand Down

0 comments on commit 0faec39

Please sign in to comment.