Pre-renders web app into static HTML. Uses headless chrome to crawl all available links starting from the root. Heavily inspired by prep and react-snapshot, but written from scratch. Uses best practices to get best loading performance.
- Enables SEO (google, duckduckgo...) and SMO (twitter, facebook...) for SPA.
- Works out-of-the-box with create-react-app - no code-changes required.
- Uses real browser behind the scene, so no issue with unsupported HTML5 features, like WebGL or Blobs.
- Does a lot of load performance optimization. Here are details, if you are curious.
- Does not depend on React. The name is inspired by
react-snapshot
. Works with any technology. - npm package does not have compilation step, so you can fork it, change what you need and install it with GitHub URL.
Zero configuration is the main feature. You do not need to worry how does it work or how to configure it. But if you are curious here are details.
Install:
yarn add --dev react-snap
Change package.json
:
"scripts": {
"postbuild": "react-snap"
}
Change src/index.js
(for React 16+):
import { hydrate, render } from 'react-dom';
const rootElement = document.getElementById('root');
if (rootElement.hasChildNodes()) {
hydrate(<App />, rootElement);
} else {
render(<App />, rootElement);
}
That's it!
Install:
yarn add --dev react-snap
Change package.json
:
"scripts": {
"postbuild": "react-snap"
},
"reactSnap": {
"source": "dist"
}
source
- output folder of webpack or any other bundler of your choice
Example: Switch from prerender-spa-plugin to react-snap
Only works with routing strategies using the HTML5 history API. No hash(bang) URLs.
Vue uses data-server-rendered
attribute on the root element to mark SSR generated markup. When this attribute is present VDOM rehydrates instead of rendering all from scratch, which can result in a flash.
This is the small hack to fix rehydration problem.
window.snapSaveState = () => {
document.querySelector('#app').setAttribute('data-server-rendered', 'true')
};
window.snapSaveState
is a callback to save a state of the application at the end of rendering. It can be used for Redux or async components. In this example it is repurposed to change DOM, this is why I call it "hack". Maybe in future versions of react-snap
I will come up with better abstractions or automate this process.
Make sure to use replace: false
for root components
- Emotion website load performance optimization
- Load performance optimization
- recipes
- stereobooster/an-almost-static-stack
If you need to pass some options for react-snap
, you can do this in the package.json
, like this:
"reactSnap": {
"inlineCss": true
}
All options are not documented yet, but you can check defaultOptions
in index.js
.
If you want to generate a sitemap.xml
for your static website you can do this in the package.json
, like this:
"homepage": "https://mycoolwebsite.com/",
"reactSnap": {
"sitemap": true
}
Without the homepage
a sitemap can't be generated.
Experimental feature - requires improvements.
react-snap
can inline critical CSS with the help of minimalcss and full CSS will be loaded in a nonblocking manner with the help of loadCss.
Use inlineCss: true
to enable this feature.
TODO: as soon as the feature will be stable it should be enabled by default.
Also known as code splitting, dynamic import (TC39 proposal), "chunks" (which are loaded on demand), "layers", "rollups", or "fragments". See: Guide To JavaScript Async Components
Async component (in React) is a technique (typically implemented as a Higher Order Component) for loading components with dynamic import
. There are a lot of solutions in this field. Here are some examples:
It is not a problem to render async component with react-snap, tricky part happens when prerendered React application boots and async components are not loaded yet, so React draws "loading" state of a component, later when component loaded react draws actual component. As the result - user sees a flash.
100% /----| |----
/ | |
/ | |
/ | |
/ |____|
visual progress /
/
0% -------------/
react-loadable
and loadable-components
solve this issue for SSR. But only loadable-components
can solve this issue for "snapshot" setup:
import { loadComponents } from "loadable-components";
import { getState } from "loadable-components";
window.snapSaveState = () => getState();
loadComponents().then(() => {
hydrate(AppWithRouter, rootElement);
});
See: Redux Server Rendering Section
// Grab the state from a global variable injected into the server-generated HTML
const preloadedState = window.__PRELOADED_STATE__
// Allow the passed state to be garbage-collected
delete window.__PRELOADED_STATE__
// Create Redux store with initial state
const store = createStore(counterApp, preloadedState || initialState)
// Tell react-snap how to save Redux state
window.snapSaveState = () => ({
"__PRELOADED_STATE__": store.getState()
});
Caution: as of now only basic "JSON" data types are supported e.g. Date, Set, Map, NaN won't be handled right (#54).
You can block all third-party requests with the following config
"skipThirdPartyRequests": true
react-snap
can capture all AJAX requests. It will store json
request to the same domain in window.snapStore[<path>]
, where <path>
is the path of json request.
Use cacheAjaxRequests: true
to enable this feature.
By default create-react-app
uses index.html
as fallback:
navigateFallback: publicUrl + '/index.html',
you need to change this to an unprerendered version of index.html
- 200.html
, otherwise you will see a flash of index.html
on other pages (if you have any). See Configure sw-precache without ejecting for more information.
Puppeteer (headless chrome) may fail due to sandboxing issues. To get around this, you may use
"puppeteerArgs": ["--no-sandbox", "--disable-setuid-sandbox"]
Read more about puppeteer troubleshooting.
"inlineCss": true
sometimes cause problems in containers
To run react-snap
inside docker
with Alpine, you might want to use custom chromium executable. See #93 and #132
heroku buildpacks:add https://github.com/jontewks/puppeteer-heroku-buildpack.git
heroku buildpacks:add heroku/nodejs
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git
See this PR. At the moment of writing, Heroku doesn't support HTTP2.
Semantic UI is defined over class substrings that contain spaces (eg. "three column"). Sorting the class names therefore breaks the styling. To get around this, use the following configuration:
"minifyHtml": { "sortClassName": false }
Once JS on the client is loaded, components initialized and your JSS styles are regenerated, it's a good time to remove server-side generated style tag in order to avoid side-effects
This is basically mean that JSS doesn't support rehydration
. See #99 for possible solutions.
See #135.
See alternatives
Please provide a reproducible demo of a bug and steps to reproduce it. Thanks
Tweet it, like it, share it, star it. Thank you
It is hard to accept big PRs as of now because there are no tests yet. PRs for help wanted
tickets are very welcome.
You also can contribute to minimalcss, which is a big part of react-snap
. Also, give some stars.