/react-snap

🔮 A zero-configuration static pre-renderer for SPA

Primary LanguageJavaScriptMIT LicenseMIT

react-snap npm npm

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.

Does not depend on React. The name is inspired by react-snapshot and because the initial goal was to enable seamless integration with create-react-app. Actually, it works with any technology. Considering to change the name.

Features

  • 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.
  • Crawls all pages starting from the root, no need to list pages by hand, like in prep.
  • With prerendered HTML and inlined critical CSS you will get fast first paint, like with critical.

Please note: some features are experimental, but prerendering is considered stable enough.

Basic usage with create-react-app

Install:

yarn add --dev react-snap

Change package.json:

"scripts": {
  "build": "react-scripts build && 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!

✨ Examples

⚙️ Customization

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.

inlineCss

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.

precacheAjax

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 precacheAjax: true to enable this feature.

⚠️ Caveats

Async components

Also known as code splitting, dynamic import (TC39 proposal), "chunks" (which are loaded on demand), "layers", "rollups", or "fragments".

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/snap";
window.snapSaveState = () => getState();

loadComponents().then(() => {
  hydrate(AppWithRouter, rootElement);
});

Redux

See: Redux Srever 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).

Google Analytics, Mapbox, and other third-party requests

You can block all third-party requests with the following config

"skipThirdPartyRequests": true

WebGL

Headless chrome does not fully support WebGL, if you need render it you can use

"headless": false

Containers and other restricted environments

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.

Error stack trace in production build

If you get an error in a production build, you can use sourcemaps to decode stack trace:

"sourceMaps": true

See #61

Possible improvements

  • Improve preconnect, dns-prefetch functionality, maybe use media queries. Example: load in small screen - capture all assets, add with a media query for the small screen, load in big screen add the rest of the assets with a media query for the big screen.
  • Do not load assets, the same way as minimalcss does
  • Evaluate penthouse as alternative to minimalcss

Alternatives