{"id":13400759,"url":"https://github.com/stereobooster/react-snap","last_synced_at":"2025-05-13T19:03:45.804Z","repository":{"id":41384227,"uuid":"105412971","full_name":"stereobooster/react-snap","owner":"stereobooster","description":"👻 Zero-configuration framework-agnostic static prerendering for SPAs","archived":false,"fork":false,"pushed_at":"2024-02-27T15:18:36.000Z","size":1472,"stargazers_count":5091,"open_issues_count":191,"forks_count":394,"subscribers_count":21,"default_branch":"master","last_synced_at":"2025-04-28T00:35:00.103Z","etag":null,"topics":["prerender","react","seo","server-side-rendering","ssr","static-site-generator","vue","zero-configuration"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stereobooster.png","metadata":{"files":{"readme":"Readme.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-10-01T01:56:32.000Z","updated_at":"2025-04-17T03:21:29.000Z","dependencies_parsed_at":"2023-01-31T00:30:53.143Z","dependency_job_id":"c55abf97-425d-4553-aff2-421b63149686","html_url":"https://github.com/stereobooster/react-snap","commit_stats":{"total_commits":397,"total_committers":37,"mean_commits":10.72972972972973,"dds":"0.23677581863979846","last_synced_commit":"e2746ebe63cc75a4943d076585fff5aa5b50751d"},"previous_names":[],"tags_count":83,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stereobooster%2Freact-snap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stereobooster%2Freact-snap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stereobooster%2Freact-snap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stereobooster%2Freact-snap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stereobooster","download_url":"https://codeload.github.com/stereobooster/react-snap/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254010793,"owners_count":21998993,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["prerender","react","seo","server-side-rendering","ssr","static-site-generator","vue","zero-configuration"],"created_at":"2024-07-30T19:00:55.185Z","updated_at":"2025-05-13T19:03:45.785Z","avatar_url":"https://github.com/stereobooster.png","language":"JavaScript","funding_links":[],"categories":["Uncategorized","Other","React [🔝](#readme)","java","JavaScript","预烧结","Components \u0026 Libraries","Tools","Prerendering [🔝](#readme)","React","Prerendering","🌐 Web Development - Frontend"],"sub_categories":["Uncategorized","命令行/终端","Prerendering","React","Command Line / Terminal"],"readme":"[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine)\n\n# react-snap [![Build Status](https://travis-ci.org/stereobooster/react-snap.svg?branch=master)](https://travis-ci.org/stereobooster/react-snap) [![npm](https://img.shields.io/npm/v/react-snap.svg)](https://www.npmjs.com/package/react-snap) ![npm](https://img.shields.io/npm/dt/react-snap.svg) [![Twitter Follow](https://img.shields.io/twitter/url/http/shields.io.svg?style=social\u0026label=Follow)](https://twitter.com/stereobooster)\n\nPre-renders a web app into static HTML. Uses [Headless Chrome](https://github.com/GoogleChrome/puppeteer) to crawl all available links starting from the root. Heavily inspired by [prep](https://github.com/graphcool/prep) and [react-snapshot](https://github.com/geelen/react-snapshot), but written from scratch. Uses best practices to get the best loading performance.\n\n## 😍 Features\n\n- Enables **SEO** (Google, DuckDuckGo...) and **SMO** (Twitter, Facebook...) for SPAs.\n- **Works out-of-the-box** with [create-react-app](https://github.com/facebookincubator/create-react-app) - no code-changes required.\n- Uses a **real browser** behind the scenes, so there are no issues with unsupported HTML5 features, like WebGL or Blobs.\n- Does a lot of **load performance optimization**. [Here are details](doc/load-performance-optimizations.md), if you are curious.\n- **Does not depend on React**. The name is inspired by `react-snapshot` but works with any technology (e.g., Vue).\n- npm package does not have a compilation step, so **you can fork** it, change what you need, and install it with a GitHub URL.\n\n**Zero configuration** is the main feature. You do not need to worry about how it works or how to configure it. But if you are curious, [here are details](doc/behind-the-scenes.md).\n\n## Basic usage with create-react-app\n\nInstall:\n\n```sh\nyarn add --dev react-snap\n```\n\nChange `package.json`:\n\n```json\n\"scripts\": {\n  \"postbuild\": \"react-snap\"\n}\n```\n\nChange `src/index.js` (for React 16+):\n\n```js\nimport { hydrate, render } from \"react-dom\";\n\nconst rootElement = document.getElementById(\"root\");\nif (rootElement.hasChildNodes()) {\n  hydrate(\u003cApp /\u003e, rootElement);\n} else {\n  render(\u003cApp /\u003e, rootElement);\n}\n```\n\nThat's it!\n\n## Basic usage with Preact\n\nTo do [hydration in Preact you need to use this trick](https://github.com/developit/preact/issues/1060#issuecomment-389987994):\n\n```js\nconst rootElement = document.getElementById(\"root\");\nif (rootElement.hasChildNodes()) {\n  preact.render(\u003cApp /\u003e, rootElement, rootElement.firstElementChild);\n} else {\n  preact.render(\u003cApp /\u003e, rootElement);\n}\n```\n\n## Basic usage with Vue.js\n\nInstall:\n\n```sh\nyarn add --dev react-snap\n```\n\nChange `package.json`:\n\n```json\n\"scripts\": {\n  \"postbuild\": \"react-snap\"\n},\n\"reactSnap\": {\n  \"source\": \"dist\",\n  \"minifyHtml\": {\n    \"collapseWhitespace\": false,\n    \"removeComments\": false\n  }\n}\n```\n\nOr use `preserveWhitespace: false` in `vue-loader`.\n\n`source` - output folder of webpack or any other bundler of your choice\n\nRead more about `minifyHtml` caveats in [#142](https://github.com/stereobooster/react-snap/issues/142).\n\nExample: [Switch from prerender-spa-plugin to react-snap](https://github.com/stereobooster/prerender-spa-plugin/commit/ee73d39b862bc905b44a04c6eaa58e6730957819)\n\n### Caveats\n\nOnly works with routing strategies using the HTML5 history API. No hash(bang) URLs.\n\nVue uses the `data-server-rendered` attribute on the root element to mark SSR generated markup. When this attribute is present, the VDOM rehydrates instead of rendering everything from scratch, which can result in a flash.\n\nThis is a small hack to fix rehydration problem:\n\n```js\nwindow.snapSaveState = () =\u003e {\n  document.querySelector(\"#app\").setAttribute(\"data-server-rendered\", \"true\");\n};\n```\n\n`window.snapSaveState` is a callback to save the 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 alter the DOM, this is why I call it a \"hack.\" Maybe in future versions of `react-snap`, I will come up with better abstractions or automate this process.\n\n### Vue 1.x\n\nMake sure to use [`replace: false`](https://v1.vuejs.org/api/#replace) for root components\n\n## ✨ Examples\n\n- [Emotion website load performance optimization](doc/emotion-site-optimization.md)\n- [Load performance optimization](doc/an-almost-static-stack-optimization.md)\n- [recipes](doc/recipes.md)\n- [stereobooster/an-almost-static-stack](https://github.com/stereobooster/an-almost-static-stack)\n\n## ⚙️ Customization\n\nIf you need to pass some options for `react-snap`, you can do this in your `package.json` like this:\n\n```json\n\"reactSnap\": {\n  \"inlineCss\": true\n}\n```\n\nNot all options are documented yet, but you can check `defaultOptions` in `index.js`.\n\n### inlineCss\n\nExperimental feature - requires improvements.\n\n`react-snap` can inline critical CSS with the help of [minimalcss](https://github.com/peterbe/minimalcss) and full CSS will be loaded in a non-blocking manner with the help of [loadCss](https://www.npmjs.com/package/fg-loadcss).\n\nUse `inlineCss: true` to enable this feature.\n\nTODO: as soon as this feature is stable, it should be enabled by default.\n\n## ⚠️ Caveats\n\n### Async components\n\nAlso known as [code splitting](https://webpack.js.org/guides/code-splitting/), [dynamic import](https://github.com/tc39/proposal-dynamic-import) (TC39 proposal), \"chunks\" (which are loaded on demand), \"layers\", \"rollups\", or \"fragments\". See: [Guide To JavaScript Async Components](https://github.com/stereobooster/guide-to-async-components)\n\nAn async component (in React) is a technique (typically implemented as a higher-order component) for loading components on demand with the dynamic `import` operator. There are a lot of solutions in this field. Here are some examples:\n\n- [`react.lazy`](https://reactjs.org/docs/code-splitting.html#reactlazy)\n- [`loadable-components`](https://github.com/smooth-code/loadable-components)\n- [`react-loadable`](https://github.com/thejameskyle/react-loadable)\n- [`react-async-component`](https://github.com/ctrlplusb/react-async-component)\n\nIt is not a problem to render async components with `react-snap`, the tricky part happens when a prerendered React application boots and async components are not loaded yet, so React draws the \"loading\" state of a component, and later when the component is loaded, React draws the actual component. As a result, the user sees a flash:\n\n```\n100%                    /----|    |----\n                       /     |    |\n                      /      |    |\n                     /       |    |\n                    /        |____|\n  visual progress  /\n                  /\n0%  -------------/\n```\n\nUsually a _code splitting_ library provides an API to handle it during SSR, but as long as \"real\" SSR is not used in react-snap - the issue surfaces, and there is no simple way to fix it.\n\n1. Use [react-prerendered-component](https://github.com/theKashey/react-prerendered-component). This library holds onto the prerendered HTML until the dynamically imported code is ready.\n\n```js\nimport loadable from \"@loadable/component\";\nimport { PrerenderedComponent } from \"react-prerendered-component\";\n\nconst prerenderedLoadable = dynamicImport =\u003e {\n  const LoadableComponent = loadable(dynamicImport);\n  return React.memo(props =\u003e (\n    // you can use the `.preload()` method from react-loadable or react-imported-component`\n    \u003cPrerenderedComponent live={LoadableComponent.load()}\u003e\n      \u003cLoadableComponent {...props} /\u003e\n    \u003c/PrerenderedComponent\u003e\n  ));\n};\n\nconst MyComponent = prerenderedLoadable(() =\u003e import(\"./MyComponent\"));\n```\n\n`MyComponent` will use prerendered HTML to prevent the page content from flashing (it will find the required piece of HTML using an `id` attribute generated by `PrerenderedComponent` and inject it using `dangerouslySetInnerHTML`).\n\n2. The same approach will work with `React.lazy`, but `React.lazy` doesn't provide a prefetch method (`load` or `preload`), so you need to implement it yourself (this can be a fragile solution).\n\n```js\nconst prefetchMap = new WeakMap();\nconst prefetchLazy = LazyComponent =\u003e {\n  if (!prefetchMap.has(LazyComponent)) {\n    prefetchMap.set(LazyComponent, LazyComponent._ctor());\n  }\n  return prefetchMap.get(LazyComponent);\n};\n\nconst prerenderedLazy = dynamicImport =\u003e {\n  const LazyComponent = React.lazy(dynamicImport);\n  return React.memo(props =\u003e (\n    \u003cPrerenderedComponent live={prefetchLazy(LazyComponent)}\u003e\n      \u003cLazyComponent {...props} /\u003e\n    \u003c/PrerenderedComponent\u003e\n  ));\n};\n\nconst MyComponent = prerenderedLazy(() =\u003e import(\"./MyComponent\"));\n```\n\n3. use `loadable-components` 2.2.3 (current is \u003e5). The old version of `loadable-components` can solve this issue for a \"snapshot\" setup:\n\n```js\nimport { loadComponents, getState } from \"loadable-components\";\nwindow.snapSaveState = () =\u003e getState();\n\nloadComponents()\n  .then(() =\u003e hydrate(AppWithRouter, rootElement))\n  .catch(() =\u003e render(AppWithRouter, rootElement));\n```\n\nIf you don't use babel plugin, [don't forget to provide modules](https://github.com/smooth-code/loadable-components/issues/114):\n\n```js\nconst NotFoundPage = loadable(() =\u003e import(\"src/pages/NotFoundPage\"), {\n  modules: [\"NotFoundPage\"]\n});\n```\n\n\u003e `loadable-components` were deprecated in favour of `@loadable/component`, but `@loadable/component` dropped `getState`. So if you want to use `loadable-components` you can use old version (`2.2.3` latest version at the moment of writing) or you can wait until `React` will implement proper handling of this case with asynchronous rendering and `React.lazy`.\n\n### Redux\n\nSee: [Redux Server Rendering Section](https://redux.js.org/docs/recipes/ServerRendering.html#the-client-side)\n\n```js\n// Grab the state from a global variable injected into the server-generated HTML\nconst preloadedState = window.__PRELOADED_STATE__;\n\n// Allow the passed state to be garbage-collected\ndelete window.__PRELOADED_STATE__;\n\n// Create Redux store with initial state\nconst store = createStore(counterApp, preloadedState || initialState);\n\n// Tell react-snap how to save Redux state\nwindow.snapSaveState = () =\u003e ({\n  __PRELOADED_STATE__: store.getState()\n});\n```\n\n**Caution**: as of now, only basic \"JSON\" data types are supported: e.g. `Date`, `Set`, `Map`, and `NaN` **won't** be handled correctly ([#54](https://github.com/stereobooster/react-snap/issues/54)).\n\n### Third-party requests: Google Analytics, Mapbox, etc.\n\nYou can block all third-party requests with the following config:\n\n```json\n\"skipThirdPartyRequests\": true\n```\n\n### AJAX\n\n`react-snap` can capture all AJAX requests. It will store `json` requests in the domain in `window.snapStore[\u003cpath\u003e]`, where `\u003cpath\u003e` is the path of the request.\n\nUse `\"cacheAjaxRequests\": true` to enable this feature.\n\nThis feature can conflict with the browser cache. See [#197](https://github.com/stereobooster/react-snap/issues/197#issuecomment-397893434) for details. You may want to disable cache in this case: `\"puppeteer\": { \"cache\": false }`.\n\n### Service Workers\n\nBy default, `create-react-app` uses `index.html` as a fallback:\n\n```json\nnavigateFallback: publicUrl + '/index.html',\n```\n\nYou need to change this to an un-prerendered version of `index.html` - `200.html`, otherwise you will see `index.html` flash on other pages (if you have any). See [Configure sw-precache without ejecting](https://github.com/stereobooster/react-snap/blob/master/doc/recipes.md#configure-sw-precache-without-ejecting) for more information.\n\n### Containers and other restricted environments\n\nPuppeteer (Headless Chrome) may fail due to sandboxing issues. To get around this,\nyou may use:\n\n```json\n\"puppeteerArgs\": [\"--no-sandbox\", \"--disable-setuid-sandbox\"]\n```\n\nRead more about [puppeteer troubleshooting](https://github.com/GoogleChrome/puppeteer/blob/master/docs/troubleshooting.md).\n\n`\"inlineCss\": true` sometimes causes problems in containers.\n\n#### Docker + Alpine\n\nTo run `react-snap` inside `docker` with Alpine, you might want to use a custom Chromium executable. See [#93](https://github.com/stereobooster/react-snap/issues/93#issuecomment-354994505) and [#132](https://github.com/stereobooster/react-snap/issues/132#issuecomment-362333702).\n\n#### Heroku\n\n```\nheroku buildpacks:add https://github.com/jontewks/puppeteer-heroku-buildpack.git\nheroku buildpacks:add heroku/nodejs\nheroku buildpacks:add https://github.com/heroku/heroku-buildpack-static.git\n```\n\nSee this [PR](https://github.com/stereobooster/an-almost-static-stack/pull/7/files). At the moment of writing, Heroku doesn't support HTTP/2.\n\n### Semantic UI\n\n[Semantic UI](https://semantic-ui.com/) is defined over class substrings that contain spaces\n(e.g., \"three column\"). Sorting the class names, therefore, breaks the styling. To get around this,\nuse the following configuration:\n\n```json\n\"minifyHtml\": { \"sortClassName\": false }\n```\n\nFrom version `1.17.0`, `sortClassName` is `false` by default.\n\n### JSS\n\n\u003e 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\n\u003e\n\u003e https://github.com/cssinjs/jss/blob/master/docs/ssr.md\n\nThis basically means that JSS doesn't support `rehydration`. See [#99](https://github.com/stereobooster/react-snap/issues/99) for a possible solutions.\n\n### `react-router` v3\n\nSee [#135](https://github.com/stereobooster/react-snap/issues/135).\n\n### userAgent\n\nYou can use `navigator.userAgent == \"ReactSnap\"` to do some checks in the app code while snapping—for example, if you use an absolute path for your API AJAX request. While crawling, however, you should request a specific host.\n\nExample code:\n\n```js\nconst BASE_URL =\n  process.env.NODE_ENV == \"production\" \u0026\u0026 navigator.userAgent != \"ReactSnap\"\n    ? \"/\"\n    : \"http://xxx.yy/rest-api\";\n```\n\n## Alternatives\n\nSee [alternatives](doc/alternatives.md).\n\n## Who uses it\n\n| [![cloud.gov.au](doc/who-uses-it/cloud.gov.au.png)](https://github.com/govau/cloud.gov.au/blob/0187dd78d8f1751923631d3ff16e0fbe4a82bcc6/www/ui/package.json#L29) | [![blacklane](doc/who-uses-it/blacklane.png)](http://m.blacklane.com/) | [![reformma](doc/who-uses-it/reformma.png)](http://reformma.com) |\n| ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------ |\n\n\n## Contributing\n\n### Report a bug\n\nPlease provide a reproducible demo of a bug and steps to reproduce it. Thanks!\n\n### Share on the web\n\nTweet it, like it, share it, star it. Thank you.\n\n### Code\n\nYou can also contribute to [minimalcss](https://github.com/peterbe/minimalcss), which is a big part of `react-snap`. Also, give it some stars.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstereobooster%2Freact-snap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstereobooster%2Freact-snap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstereobooster%2Freact-snap/lists"}