Skip to content

Latest commit

 

History

History
284 lines (222 loc) · 10.7 KB

Code-Splitting-Using-Holocron.md

File metadata and controls

284 lines (222 loc) · 10.7 KB

👈 Return to Overview

Code Splitting Using Holocron

Contents

For One App, Holocron modules are a formal way of code-splitting for a modern, micro front-end in the One App ecosystem. Utilizing the module map as a system which promotes synergy and asynchrony across teams and collaborators on a single application.

With Holocron each individual module is by definition, a code split bundled component with language packs that can be combined and composed based on our needs. Each module is versioned, configurable, routable and can be further split into smaller chunks using dynamic importing.

The main advantage we get with code splitting our modules is controlling what parts of a module is first delivered and larger chunks are loaded by user actions and are not necessary to include at initial delivery. This can save end users kilobytes of data transfer (until needed) and reduce the time for waiting on the Holocron module bundle to load.

Like @loadable/component and react-loadable, holocron is a complete code-splitting solution that works in both client and server side, however there is no need to scaffold a build infrastructure to support code-splitting when we use one-app-bundler.

📘 More Information

Route Based Code Splitting

A Holocron module can be route-driven using ModuleRoute and we can use this component to shape our routing for One App. Since each Holocron module is an independent bundle, our childRoutes is a map of code-split modules that is driven by the url.

ModuleRoute is an extension of Route from one-app-router and can render the module(s) by its route per history. Our Holocron modules are now loaded based on routing through the application.

import React from 'react';
import { RenderModule } from 'holocron';
import { ModuleRoute } from 'holocron-module-route';
import { IndexRedirect } from '@americanexpress/one-app-router';

export const childRoutes = () => (
  <ModuleRoute moduleName="diner-finder">
    <IndexRedirect to="map" />
    <ModuleRoute path="map" moduleName="diner-finder-map" />
    <ModuleRoute path="selection" moduleName="diner-finder-selection-container">
      <ModuleRoute moduleName="diner-finder-selection" />
    </ModuleRoute>
  </ModuleRoute>
);

To view a root Holocron module with a childRoutes config, check out the sample module "Frank Lloyd Root"

📘 More Information

Holocron Module Chunks Via import()

With a Holocron module, we use code-splitting to add granularity to our modules and create chunks defined by user-driven actions or incorporate lazy loading for larger parts of our module. Let's start with a sample Holocron module project:

root
|── locale
|   └── en-US.json
|── src
|   ├── Chunk.jsx
|   ├── index.js
|   └── Module.jsx
└── package.json

src/index.js

import HolocronModule from './Module';

export default HolocronModule;

It's important to mention, when we are code splitting with dynamic imports, we must rely on user actions to load module chunks dynamically.

src/Module.jsx

import React from 'react';

export default function MyHolocronModule() {
  const [loadModule, setLoadModule] = React.useState(false);
  const [Component, setComponent] = React.useState(null);

  React.useEffect(() => {
    if (loadModule) {
      import(/* webpackChunkName: "<chunkName>" */ './Chunk')
        // the multi-line comment /* webpackChunkName: "..." */
        // is used by webpack to name your chunk
        .then((importedChunk) => importedChunk.default || importedChunk)
        // when we convert ES modules to common/supported formats,
        // we might need to interop the `default` property to get the export
        .then((chunk) => {
          // ... do things with the chunk once we have what we want
          setComponent(chunk);
        });
    }
  }, [loadModule]);

  if (Component) return <Component />;

  return (
    <button type="button" onClick={() => setLoadModule(true)}>
      Load Module
    </button>
  );
}

src/Chunk.jsx

import React from 'react';

export default function ModuleChunk() {
  return <p>My Holocron Module Chunk</p>;
}

In our basic example, if we wanted to use Chunk.jsx as a Holocron module chunk, we can use the supported import() syntax to divide up our module bundle into logical chunks and name our chunk using the magic comments /* webpackChunkName: "..." */:

// compared to traditional imports, which adds to the main bundle
import './Chunk';

import(/* webpackChunkName: "<chunkName>" */ './Chunk');

Our chunk can be anything from a component to a node_modules package. Once we run bundle-module from one-app-bundler, our output in build/<version> will contain additional JavaScript files based on the number of chunks we have dynamically split from our module.

root
└── build
    └── 1.0.0
        ├── <chunkName>.<moduleName>.chunk.browser.js
        ├── vendors~<chunkName>.<moduleName>.chunk.browser.js
        ├── <chunkName>.<moduleName>.chunk.legacy.browser.js
        ├── <moduleName>.browser.js
        ├── <moduleName>.legacy.browser.js
        ├── <moduleName>.node.js
        └── en-US
            ├── <moduleName>.json
            ├── integration.json
            └── qa.json

To view a module chunks example using dynamic importing, check out the sample module "Franks Burgers"

Using Module.holocron

src/Module.jsx

import React from 'react';

let Chunk = () => <p>Loading...</p>;

export function MyHolocronModule() {
  return <Chunk />;
}

MyHolocronModule.holocron = {
  name: 'holocron-module-name',
  // the loadModuleData property allows us to load our chunk with our module
  loadModuleData: async () => {
    try {
      // the same dynamic import is used to create a promise with our chunk
      const imported = await import(/* webpackChunkName: '<chunkName>' */ './Chunk');
      Chunk = imported.default || imported;
    } catch (error) {
      Chunk = function ChunkError() { return <p>{error.message}</p>; };
    }
  },
  // we can wait for our holocron module to load the chunk during server-side rendering
  // and render the module with the loaded chunk
};

export default MyHolocronModule;

Using Suspense and lazy

React comes with an API made with code splitting in mind; with React.Suspense and React.lazy, we can load a module chunk from our CDN and fallback to a given value while it asynchronously loads.

server side rendering is not compatible with Suspense & lazy

src/Module.jsx

import React from 'react';

const Chunk = React.lazy(() => import(/* webpackChunkName: "<chunkName>" */ './Chunk')
  .then((importedChunk) => importedChunk.default || importedChunk));

export default function MyHolocronModule() {
  return (
    <React.Suspense fallback={<p>Loading...</p>}>
      <Chunk />
    </React.Suspense>
  );
}

Usage Considerations

Performance

When it comes to bundling a Holocron module and splitting it up into chunks for loading, it's important to consider why we are code splitting our Holocron module. With each chunk that is split from the module bundle, the browser has to open an extra connection to the server to fetch every chunk. This can lead to performance degradation and can cause excessive traffic to the web server if the code splitting is too granular.

UX

For an end user of One App, we want to create a fluid user experience while module chunks are being asynchronously loaded. We want to focus on when the module is being loaded and avoid "flashing" an indeterminate loading state (eg fast network, browser cache, service worker) as well as handle delays in the module chunk delivery.

How It Works

One App uses one-app-bundler to support dynamic importing and code splitting on the module level. Out of the box, one-app-bundler uses webpack and babel (with @babel/plugin-syntax-dynamic-import) under the hood to understand the import() syntax and automatically split your module where ever the import() statement is used within your module.

We can expect the final bundled module output to contain everything it normally does, with two additional JavaScript files per chunk (modern browser and legacy.browser versions). For the node build output, all the dynamic imports have been imported, bundled and treated as a webpack_require call.

Include Other Tools Into The Pipeline

It can be helpful to test your current apps for migration and support react-loadable and @loadable/component. We can extend the webpack config if needed.

☝️ Return To Top