Skip to content

React.lazy substitute which works with server-side rendering

License

Notifications You must be signed in to change notification settings

overlookmotel/react-lazy-ssr

Repository files navigation

NPM version Build Status Dependency Status Dev dependency Status Greenkeeper badge Coverage Status

React.lazy substitute which works with server-side rendering

Usage

react-lazy-ssr is a drop-in substitute for React.lazy.

Unlike React's built-in .lazy() method, it can be used in server-side rendering with react-async-ssr.

Requires React 16.8.x or 16.9.x. React 16.10.0+ is not supported at present.

Client-side only

Use react-lazy-ssr exactly as you would React.lazy:

import React, {Suspense} from 'react';
import lazy from 'react-lazy-ssr';

const LazyFoo = lazy(
  () => import('./Foo.jsx')
);

const App = () => (
  <Suspense fallback="Loading...">
    <LazyFoo />
  </Suspense>
);

Server-side rendering

There are 5 steps to making server-side rendering work.

There is an example of a complete repo here. It's less complicated than it sounds!

1. Server-side renderer

Use react-async-ssr's .renderToStringAsync() method to render on server, rather than react-dom's .renderToString() (see here for instructions).

2. Name all lazy components

Each lazy component must have a unique name, which can be provided with the chunkName option.

const LazyFoo = lazy(
  () => import('./Foo.jsx'),
  { chunkName: 'Foo' }
);

NB Name must be unique for the component being loaded, NOT unique for each call to lazy(). If different files both want to lazy load the same component, they can (and should) use the same chunk name.

Babel plugin

This package provides a Babel plugin to add chunk names automatically for you.

Add react-lazy-ssr/babel plugin to your Babel config:

// .babelrc.js
const LazyPlugin = require('react-lazy-ssr/babel');

module.exports = {
  presets: [
    '@babel/preset-env',
    '@babel/preset-react'
  ],
  plugins: [
    [ LazyPlugin, { rootPath: __dirname} ]
  ]
};

rootPath option is optional, but it ensures no naming clashes where import() is given relative paths.

The Babel plugin will transform this:

lazy( () => import( './Foo.jsx' ) );

to this:

lazy(
  () => import( /* webpackChunkName: "Foo" */ './Foo.jsx' ),
  { chunkName: 'Foo' }
);

3. Webpack plugin

Add this package's Webpack plugin to your Webpack config (for the client-side build only):

// webpack.config.js
const ReactLazySsrPlugin = require('react-lazy-ssr/webpack');

module.exports = {
  target: 'web',
  entry: `./src/client/main.jsx`,
  plugins: [
    new ReactLazySsrPlugin()
  ],
  ...
}

The purpose of the Webpack plugin is to output a JSON stats file which maps module names to the filenames of files Webpack outputs. This stats file is used by the next step.

After running Webpack, you'll find a file reactLazySsrStats.json in the build folder.

4. Server-side: Capture chunks

In order for the page to hydrate correctly on the client side, all the code which is lazy-loaded on the server must also be sent to the client to load prior to hydration.

This package provides ChunkExtractor to do this.

  • Import the stats file created by Webpack plugin
  • Wrap the app before rendering in a ChunkExtractor.
  • Call chunkExtractor.getScriptTags() to get all the <script> tags to add at the bottom of the HTML body.
// Import packages
const {renderToStringAsync} = require('react-async-ssr'),
  {ChunkExtractor} = require('react-lazy-ssr/server');

// Import App
const App = require('./build/server/main.js');

// Import stats file created by Webpack plugin
const stats = require('./build/client/reactLazySsrStats.json');

// Define route
expressApp.get( '/', async (req, res) => {
  // Wrap app in a ChunkExtractor
  const chunkExtractor = new ChunkExtractor( { stats } );
  const app = chunkExtractor.collectChunks( <App /> );

  // Async render
  const html = await renderToStringAsync( app );

  // Get scripts
  const scriptsHtml = chunkExtractor.getScriptTags();

  // Return response
  res.send(`
    <html>
      <head><title>Example</title></head>
      <body>
        <div id="app">${ html }</div>
        ${ scriptsHtml }
      </body>
    </html>
  `);
} );

5. Client-side: Preload all lazy components before hydrating

On the client side, all lazy components must be preloaded before hydrating the app. This ensures the app is "primed" and renders exactly the same on client as it did on the server.

Instead of:

ReactDOM.hydrate(
  <App />,
  document.getElementById('app')
);

use:

import lazy from 'react-lazy-ssr';

lazy.preloadAll().then( () => {
  ReactDOM.hydrate(
    <App />,
    document.getElementById('app')
  );
} );

Advanced usage

Excluding components from rendering on server

Sometimes you might want to prevent a component rendering on server side. For example, it might be a low-priority part of the page, "below the fold", or a heavy component which will take a long time to load and increase the delay before hydration.

To prevent a component rendering on server-side, use the noSsr option.

const LazyFoo = lazy(
  () => import('./Foo.jsx'),
  { noSsr: true }
);

The no-SSR component will trigger the nearest Suspense fallback on the server side, and it's that fallback HTML which will be sent to the client. Then the component will be loaded on the client-side after hydration.

Preloading a lazy component

To preload a lazy component before it's rendered, call .preload().

.preload() returns a promise which resolves when the component is loaded, or rejects if loading fails.

const LazyFoo = lazy( () => import('./Foo.jsx') );

LazyFoo.preload().then(
  () => console.log('Loaded'),
  (err) => console.log('Error:', err)
);

Entry points

By default, ChunkExtractor expects the app entry point chunk to be called main. You can change this with the entryPoint option.

const chunkExtractor = new ChunkExtractor( {
  stats,
  entryPoint: 'entry'
} );

Or, if the app has multiple entry points, provide an array:

const chunkExtractor = new ChunkExtractor( {
  stats,
  entryPoint: [ 'entry1', 'entry2' ]
} );

Optimizing client-side loading

async and defer

By default, chunkExtractor.getScriptTags() will produce <script> tags suitable for putting at the bottom of the HTML body.

It produces something like:

<script>
window.__REACT_LAZY_SSR_CHUNKS_REQUIRED__ = ["LazyLoaded"];
</script>
<script src="/static/LazyLoaded.js"></script>
<script src="/static/vendors~main.js"></script>
<script src="/static/main.js"></script>

You may be able to improve loading performance by putting the scripts in the HTML head and using async or defer attributes.

Pass either {async: true} or {defer: true} to .getScriptTags():

chunkExtractor.getScriptTags( { async: true } )

This adds an async attribute to the <script> elements and some Javascript to ensure the page is not hydrated until all have loaded.

<script>
window.__REACT_LAZY_SSR_CHUNKS_REQUIRED__ = ["LazyLoaded"];
window.__REACT_LAZY_SSR_FILES_REQUIRED__ =
  ["LazyLoaded.js","vendors~main.js","main.js"];
</script>
<script async src="/static/LazyLoaded.js" onload="..."></script>
<script async src="/static/vendors~main.js" onload="..."></script>
<script async src="/static/main.js" onload="..."></script>

Preload tags

You can also use <link rel="preload"> tags at the top of the page to preload Javascript while the browser is still parsing the HTML body.

Use .getPreloadTags() and put the returned HTML in the document head, in addition to script tags at bottom of the HTML body.

.getPreloadTags() returns:

<link rel="preload" href="/static/LazyLoaded.js" as="script">
<link rel="preload" href="/static/vendors~main.js" as="script">
<link rel="preload" href="/static/main.js" as="script">

Babel plugin: File extensions

Certain file extensions will be stripped off when creating chunk names e.g. Foo.jsx => Foo.

By default, the file extensions which are stripped are .js, .jsx, .mjs and .cjs.

You can customize this with the exts option.

// .babelrc.js
module.exports = {
  ...
  plugins: [
    [
      'react-lazy-ssr/babel',
      {
        rootPath: __dirname,
        exts: ['js', 'jsx', 'react']
      }
    ]
  ]
};

Webpack plugin: Stats file name

If you want the stats file to be called something other than reactLazySsrStats.json, you can use the filename option.

// webpack.config.js
module.exports = {
  plugins: [
    new ReactLazySsrPlugin( {
      filename: 'my-alternative-filename.json'
    } )
  ],
  ...
}

Tests

Use npm test to run the tests. Use npm run cover to check coverage.

Changelog

See changelog.md

Issues

If you discover a bug, please raise an issue on Github. https://github.com/overlookmotel/react-lazy-ssr/issues

Contribution

Pull requests are very welcome. Please:

  • ensure all tests pass before submitting PR
  • add tests for new features
  • document new functionality/API additions in README
  • do not add an entry to Changelog (Changelog is created when cutting releases)

About

React.lazy substitute which works with server-side rendering

Resources

License

Stars

Watchers

Forks

Packages

No packages published