Skip to content

Commit

Permalink
feat(core): react-ssr-prepass for more performant SSR (#46)
Browse files Browse the repository at this point in the history
* improvement(core): use ssr-prepass instead of renderToStringMarkup

* improvement(examples): add code to benchmark

* feat(core): use react-ssr-prepass for Suspense SSR 🎉

* docs: update docs regarding SSR

* test: updated tests

* docs: updated readme
  • Loading branch information
jackyef authored Mar 21, 2020
1 parent 545b308 commit d8747cf
Show file tree
Hide file tree
Showing 23 changed files with 110 additions and 73 deletions.
9 changes: 6 additions & 3 deletions docusaurus/docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ import App from './App';
// react-isomorphic-data needs fetch to be available in the global scope
global.fetch = fetch;

express.get('/*', async (req: express.Request, res: express.Response) => {
const server = express();

server.get('/*', async (req: express.Request, res: express.Response) => {
const dataClient = createDataClient({
initialCache: {},
ssr: true,
Expand All @@ -71,7 +73,7 @@ express.get('/*', async (req: express.Request, res: express.Response) => {
);

try {
await getDataFromTree(reactApp, dataClient);
await getDataFromTree(reactApp);
} catch (err) {
console.error('Error while trying to getDataFromTree', err);
}
Expand All @@ -95,4 +97,5 @@ express.get('/*', async (req: express.Request, res: express.Response) => {
</body>
</html>`
);
}
});
```
2 changes: 1 addition & 1 deletion docusaurus/docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ NOTE: This project is still very much work in progress, use at your own risk ⚠

### Features
- React hooks
- SSR support
- SSR support with Suspense using [react-ssr-prepass](https://github.com/FormidableLabs/react-ssr-prepass) (No multi-rendering on the server!)
- Simple built-in cache
- TypeScript support
- [Testing utilities](./testing/writing-tests)
Expand Down
2 changes: 1 addition & 1 deletion docusaurus/docs/ssr/client-side-hydration.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const tree = (
let markup;

try {
await getDataFromTree(tree, dataClient);
await getDataFromTree(tree);
markup = renderToString(tree);
} catch (err) {
console.error('An error happened during server side rendering!');
Expand Down
7 changes: 4 additions & 3 deletions docusaurus/docs/ssr/getDataFromTree.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ description: 'Explanation for getDataFromTree() in react-isomorphic-data'
## `getDataFromTree()`
Params
* `tree: React.ReactElement`
* `dataClient: DataClient`

This function return a `Promise` that will resolve to a `string`, containing the static markup for the React app.
Most of the time, you are not going to use the static markup generated by this function for your response. The main purpose
Expand All @@ -35,7 +34,9 @@ import App from './App';
// react-isomorphic-data needs fetch to be available in the global scope
global.fetch = fetch;

express.get('/*', async (req, res) => {
const server = express();

server.get('/*', async (req, res) => {
const dataClient = createDataClient({
initialCache: {},
ssr: true, // set this to true on server side
Expand Down Expand Up @@ -63,5 +64,5 @@ express.get('/*', async (req, res) => {
</body>
</html>
`);
}
});
```
8 changes: 5 additions & 3 deletions docusaurus/docs/ssr/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import App from './App';
// react-isomorphic-data needs fetch to be available in the global scope
global.fetch = fetch;

express.get('/*', async (req, res) => {
const server = express();

server.get('/*', async (req, res) => {
const dataClient = createDataClient({
initialCache: {},
ssr: true, // set this to true on server side
Expand All @@ -42,7 +44,7 @@ express.get('/*', async (req, res) => {
let markup;

try {
markup = await renderToStringWithData(tree, dataClient);
markup = await renderToStringWithData(tree);
} catch (err) {
console.error('An error happened during server side rendering!');
}
Expand All @@ -54,5 +56,5 @@ express.get('/*', async (req, res) => {
</body>
</html>
`);
}
});
```
8 changes: 4 additions & 4 deletions docusaurus/docs/ssr/prefetching.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ description: 'Adding prefetch hints to optimise loading with react-isomorphic-da

`react-isomorphic-data` provide helper function to inject `<link rel="prefetch">` tags in to the server-side rendered HTML. This is done to give hints to the browser that the particular resource should be prefetched. Prefetched resources have a very low priority and will only be run when the browser is idle. Prefetching resources could improve performance because when the resource is requested, it might already be ready in the prefetch cache. More about prefetch [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Link_prefetching_FAQ).

In general, you should prefetch data that you think will have a high possibility to be requested by the page.
In general, you should prefetch data that you think will be needed later by the page, but not on the initial load.

## Example
First, make sure you have are setting `dataOptions.prefetch` to true for some of your data.
Expand All @@ -30,7 +30,7 @@ Then, in your server side rendering code, you can `createPrefetchTags` from the
```javascript
import { renderToStringWithData, createPrefetchTags } from 'react-isomorphic-data/ssr';

express.get('/*', async (req, res) => {
server.get('/*', async (req, res) => {
const dataClient = createDataClient({
initialCache: {},
ssr: true, // set this to true on server side
Expand All @@ -45,7 +45,7 @@ express.get('/*', async (req, res) => {
let markup;

try {
markup = await renderToStringWithData(tree, dataClient);
markup = await renderToStringWithData(tree);
} catch (err) {
console.error('An error happened during server side rendering!');
}
Expand All @@ -60,7 +60,7 @@ express.get('/*', async (req, res) => {
</body>
</html>
`);
}
});
```

## Note
Expand Down
7 changes: 4 additions & 3 deletions docusaurus/docs/ssr/renderToStringWithData.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ description: 'Explanation for renderToStringWithData() in react-isomorphic-data'
## `renderToStringWithData()`
Params
* `tree: React.ReactElement`
* `dataClient: DataClient`

This function return a `Promise` that will resolve to a `string`, containing the markup for the React app.

Expand All @@ -28,7 +27,9 @@ import App from './App';
// react-isomorphic-data needs fetch to be available in the global scope
global.fetch = fetch;

express.get('/*', async (req, res) => {
const server = express();

server.get('/*', async (req, res) => {
const dataClient = createDataClient({
initialCache: {},
ssr: true, // set this to true on server side
Expand All @@ -55,5 +56,5 @@ express.get('/*', async (req, res) => {
</body>
</html>
`);
}
});
```
1 change: 1 addition & 0 deletions examples/ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"express": "^4.17.1",
"hoist-non-react-statics": "^3.3.0",
"node-fetch": "^2.6.0",
"node-stdev": "^1.0.1",
"razzle": "^3.0.0",
"razzle-plugin-typescript": "^3.0.0",
"react": "^16.10.1",
Expand Down
1 change: 1 addition & 0 deletions examples/ssr/result-normal-ssr.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"normal-ssr","average":11.492283940594056,"stdev":4.21,"runs":101}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"ssr-prepass-no-multi-rendering-prod","average":9.321476039215687,"stdev":2.23,"runs":102}
1 change: 1 addition & 0 deletions examples/ssr/result-ssr-prepass-no-multi-rendering.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"ssr-prepass-no-multi-rendering","average":9.96071270588235,"stdev":4.14,"runs":102}
1 change: 1 addition & 0 deletions examples/ssr/result-ssr-prepass.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"name":"ssr-prepass","average":10.909492742574258,"stdev":2.45,"runs":101}
32 changes: 32 additions & 0 deletions examples/ssr/src/server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,34 @@ const globalAny: any = global;
globalAny.fetch = fetch;

let assets: any;
const time: [number, number][] = [];
const methodName = 'ssr-prepass-no-multi-rendering-prod';

const getResult = () => {
console.info("================ RESULT ================");
const durations = time.map(t => (t[0] + t[1] / 1e9) * 1e3);

durations.forEach((d, i) => {
console.info(`Run ${i} took `, d, "ms");
});

console.info("================ SUMMARY ================");
console.info(`[${methodName}]`);
console.info(
"Average is:",
durations.reduce((a, b) => a + b) / durations.length,
"ms"
);
console.info("Stdev is:", require("node-stdev").population(durations), "ms");

// uncomment this to write result into a json file
// require('fs').writeFileSync(`./result-${methodName}.json`, JSON.stringify({
// name: methodName,
// average: durations.reduce((a, b) => a + b) / durations.length,
// stdev: require("node-stdev").population(durations),
// runs: durations.length,
// }));
}

const syncLoadAssets = () => {
// eslint-disable-next-line
Expand Down Expand Up @@ -70,7 +98,11 @@ server.get('/*', async (req: express.Request, res: express.Response) => {
let markup;
// pass the same dataClient instance you are passing to your provider here
try {
const start = process.hrtime();
markup = await renderToStringWithData(reactApp, dataClient);
time.push(process.hrtime(start));

getResult();
} catch (err) {
console.error('Error while trying to getDataFromTree', err);
}
Expand Down
10 changes: 6 additions & 4 deletions packages/react-isomorphic-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ NOTE: This project is still very much work in progress, use at your own risk ⚠

### Features
- React hooks
- SSR support
- SSR support with Suspense using [react-ssr-prepass](https://github.com/FormidableLabs/react-ssr-prepass) (No multi-rendering on the server!)
- Simple built-in cache
- TypeScript support
- [Testing utilities](https://react-isomorphic-data.netlify.com/docs/testing/writing-tests)
Expand Down Expand Up @@ -67,7 +67,9 @@ import App from './App';
// react-isomorphic-data needs fetch to be available in the global scope
global.fetch = fetch;

express.get('/*', async (req: express.Request, res: express.Response) => {
const server = express();

server.get('/*', async (req: express.Request, res: express.Response) => {
const dataClient = createDataClient({
initialCache: {},
ssr: true,
Expand All @@ -84,7 +86,7 @@ express.get('/*', async (req: express.Request, res: express.Response) => {
);

try {
await getDataFromTree(reactApp, dataClient);
await getDataFromTree(reactApp);
} catch (err) {
console.error('Error while trying to getDataFromTree', err);
}
Expand All @@ -108,7 +110,7 @@ express.get('/*', async (req: express.Request, res: express.Response) => {
</body>
</html>`
);
}
});
```

### Documentations [![Netlify Status](https://api.netlify.com/api/v1/badges/81844630-ff7d-4bf6-95f0-9f170ba6e421/deploy-status)](https://app.netlify.com/sites/unruffled-austin-36e969/deploys)
Expand Down
1 change: 1 addition & 0 deletions packages/react-isomorphic-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"prettier": "^1.18.2",
"react": "^16.8.0",
"react-dom": "^16.8.0",
"react-ssr-prepass": "^1.1.2",
"rollup": "^1.19.3",
"rollup-plugin-commonjs": "^10.0.2",
"rollup-plugin-json": "^4.0.0",
Expand Down
1 change: 0 additions & 1 deletion packages/react-isomorphic-data/src/common/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ export const createDataClient = (

return {
cache: initialCache ? { ...initialCache } : {},
pendingPromiseFactories: [],
ssr: ssr || false,
test: test || false,
headers: headers || {},
Expand Down
1 change: 0 additions & 1 deletion packages/react-isomorphic-data/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export interface DataResource {
};
export interface DataClient {
cache: Record<string, any>;
pendingPromiseFactories: { () : Promise<any> }[];
toBePrefetched: Record<string, boolean>;
ssr: boolean;
test: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ const useBaseData = <T, > (
// if this data is supposed to be fetched during SSR
if (isSSR) {
if (!promisePushed.current && !lazy && !dataFromCache) {
client.pendingPromiseFactories.push(memoizedFetchData);
promisePushed.current = true;

// throw a promise here. react-ssr-prepass will handle the suspension
throw memoizedFetchData();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('Server-side rendering utilities test', () => {
</DataProvider>
);

await getDataFromTree(App, client);
await getDataFromTree(App);

expect(retrieveFromCache(client.cache, url)).toStrictEqual({ message: 'Hello world!' });
});
Expand All @@ -53,7 +53,7 @@ describe('Server-side rendering utilities test', () => {
</DataProvider>
);

const markup = await renderToStringWithData(App, client);
const markup = await renderToStringWithData(App);

expect(markup).toContain('Hello world!');
});
Expand Down Expand Up @@ -83,7 +83,7 @@ describe('Server-side rendering utilities test', () => {
</DataProvider>
);

const markup = await renderToStringWithData(App, client);
const markup = await renderToStringWithData(App);

expect(markup).toContain('ComponentB: Hello world!');
});
Expand Down Expand Up @@ -118,7 +118,7 @@ describe('Server-side rendering utilities test', () => {
</DataProvider>
);

const markup = await renderToStringWithData(App, client);
const markup = await renderToStringWithData(App);

expect(markup).toContain('ComponentB: Hello world!');
});
Expand All @@ -145,7 +145,7 @@ describe('Server-side rendering utilities test', () => {
</DataProvider>
);

const markup = await renderToStringWithData(App, client);
const markup = await renderToStringWithData(App);

expect(markup).toContain('loading...');

Expand Down Expand Up @@ -179,7 +179,7 @@ describe('Server-side rendering utilities test', () => {
</DataProvider>
);

const markup = await renderToStringWithData(App, client);
const markup = await renderToStringWithData(App);

expect(markup).toContain('loading...');

Expand Down
8 changes: 2 additions & 6 deletions packages/react-isomorphic-data/src/ssr/getDataFromTree.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import * as React from 'react';
import ReactDOMServer from 'react-dom/server';

import baseGetData from './utils/baseGetData';
import { DataClient } from '../common/types';

const { renderToStaticMarkup } = ReactDOMServer;

const getDataFromTree = async (tree: React.ReactElement, client: DataClient): Promise<string> => {
return baseGetData(tree, client, renderToStaticMarkup);
const getDataFromTree = async (tree: React.ReactElement): Promise<void> => {
return baseGetData(tree);
}

export default getDataFromTree;
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import * as React from 'react';
import ReactDOMServer from 'react-dom/server';

import baseGetData from './utils/baseGetData';
import { DataClient } from '../common/types';

const { renderToString } = ReactDOMServer;

const getDataFromTree = async (tree: React.ReactElement, client: DataClient): Promise<string> => {
return baseGetData(tree, client, renderToString);
const getDataFromTree = async (tree: React.ReactElement): Promise<string> => {
await baseGetData(tree);

return renderToString(tree);
};

export default getDataFromTree;
Loading

0 comments on commit d8747cf

Please sign in to comment.