Skip to content

👻 Zero-configuration framework-agnostic static prerendering for SPAs

License

Notifications You must be signed in to change notification settings

MrPeker/react-snap

Repository files navigation

react-snap npm npm Twitter Follow

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.

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.
  • 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.

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.

⚠️ 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);
});

Caution: there seems to be an issue in loadable-components. Be carefull.

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).

Third-party requests: Google Analytics, Mapbox etc.

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

"skipThirdPartyRequests": true

AJAX

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.

Service Workers

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.

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.

Semantic UI

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 }

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
  • Check if there is a way to improve font loading. See: 1, 2

Alternatives

About

👻 Zero-configuration framework-agnostic static prerendering for SPAs

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 92.0%
  • HTML 7.6%
  • CSS 0.4%