← Return Home

Shared libraries

Webpack shared libraries are slightly different from code splitting scenarios in that the common dependencies are shareable across builds and require a two-part build. In a first step, a common shared bundle and manifest is created. Then, in a second step, entry points ingest the manifest and omit any libraries included in the shared bundle. Shared libraries are appropriate for better long term caching within a single app across deploys and across different projects / real HTML pages.

Basic Example

(Example source available at: github.com/FormidableLabs/formidable-playbook/tree/master/examples/frontend/src/es5)

Let’s start with some source code (the same as we will use for the code splitting example):

foo.js

module.exports = function (id, msg) {
  return "<h1 id=\"" + id + "\">" + msg + "</h1>";
};

app1.js

var foo = require("./foo");

document.querySelector("#content").innerHTML += foo("app1", "App 1");

app2.js

var foo = require("./foo");

document.querySelector("#content").innerHTML += foo("app2", "App 2");

… so basically two separate “apps” that will add the headings App 1 and App 2 to a page using the same foo() method…

We then add one more file:

lib.js

require("./foo");

Which doesn’t do anything with the ./foo import. It instead just declares "add this dependency" for our later use in creating a manual bundle of shared dependencies. This explicit definition is essentially the big difference with code splitting which automatically infers shared dependencies. For the shared library approach in this section, we will need to manually curate and update the libraries to include in the shared bundle.

Shared Lib Example

(Example build / dist code available at: github.com/FormidableLabs/formidable-playbook/tree/master/examples/frontend/webpack-shared-libs)

Shared libraries allow us to manually specify code in shared bundle, that can then be excluded in any other entry points (across projects). To accomplish this we need two separate webpack configurations.

First, we specify how to build the shared library. We use the Webpack DllPlugin (example) to create a manifest for the shared library.

webpack.config.lib.js

var path = require("path");
var webpack = require("webpack");

module.exports = {
  context: path.join(__dirname, "../src/es5"),
  entry: {
    lib: ["./lib"]
  },
  output: {
    path: path.join(__dirname, "dist/js"),
    filename: "[name].js",
    library: "[name]_[hash]",
    pathinfo: true
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, "dist/js/[name]-manifest.json"),
      name: "[name]_[hash]"
    })
  ]
};

This produces two files:

NOTE - Cross project sharing: The biggest thing to understand for shared libraries is that this first step can be completely independent of the second entry point build step – across:

  • Multiple entry points in the same project / application
  • Multiple entry points in different projects / applications

This means that we have a truly portable, cacheable library for an entire website or collection of sites, unlike the project-specific code splitting solution.

Turning back to our build, we then use a webpack configuration for our entry points which consumes the lib-manifest.json file to exclude the things that are in the shared library from the resulting entry points using the DllReferencePlugin (example).

webpack.config.js

var path = require("path");
var webpack = require("webpack");

module.exports = {
  context: path.join(__dirname, "../src/es5"),
  entry: {
    app1: "./app1.js",
    app2: "./app2.js"
  },
  output: {
    path: path.join(__dirname, "dist/js"),
    filename: "[name].js",
    pathinfo: true
  },
  plugins: [
    new webpack.DllReferencePlugin({
      context: path.join(__dirname, "../src/es5"),
      manifest: require("./dist/js/lib-manifest.json")
    })
  ]
};

All together, this leaves us with four files:

Let’s look at these files in detail:

dist/js/lib.js

var lib_3e48f809b016b57221ef =
/******/ (function(modules) { // webpackBootstrap
/******/  // SNIPPED
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/* unknown exports provided */
/* all exports used */
/*!****************!*\
  !*** ./lib.js ***!
  \****************/
/***/ function(module, exports, __webpack_require__) {

/**
 * Shared Library (DLL)
 *
 * Don't need to assign to variable, just the side-effect of "including"
 * desired libraries in this file.
 */
__webpack_require__(/*! ./foo */ 1);


/***/ },
/* 1 */
/* unknown exports provided */
/* all exports used */
/*!****************!*\
  !*** ./foo.js ***!
  \****************/
/***/ function(module, exports) {

module.exports = function (id, msg) {
  return "<h1 id=\"" + id + "\">" + msg + "</h1>";
};


/***/ },
/* 2 */
/* unknown exports provided */
/* all exports used */
/*!***************!*\
  !*** dll lib ***!
  \***************/
/***/ function(module, exports, __webpack_require__) {

module.exports = __webpack_require__;

/***/ }
/******/ ]);

dist/js/lib-manifest.json

{
  "name": "lib_1c456e9656dd9be74724",
  "content": {
    "./lib.js": 1,
    "./foo.js": 2
  }
}

dist/js/app1.js

/******/ (function(modules) { // webpackBootstrap
/******/  // SNIPPED
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/* unknown exports provided */
/* all exports used */
/*!**********************************************************************!*\
  !*** delegated ./foo.js from dll-reference lib_3e48f809b016b57221ef ***!
  \**********************************************************************/
/***/ function(module, exports, __webpack_require__) {

module.exports = (__webpack_require__(1))(1);

/***/ },
/* 1 */
/* unknown exports provided */
/* all exports used */
/*!*******************************************!*\
  !*** external "lib_3e48f809b016b57221ef" ***!
  \*******************************************/
/***/ function(module, exports) {

module.exports = lib_3e48f809b016b57221ef;

/***/ },
/* 2 */
/* unknown exports provided */
/* all exports used */
/*!*****************!*\
  !*** ./app1.js ***!
  \*****************/
/***/ function(module, exports, __webpack_require__) {

var foo = __webpack_require__(/*! ./foo */ 0);

document.querySelector("#content").innerHTML += foo("app1", "App 1");


/***/ }
/******/ ]);

dist/js/app2.js

/******/ (function(modules) { // webpackBootstrap
/******/  // SNIPPED
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/* unknown exports provided */
/* all exports used */
/*!**********************************************************************!*\
  !*** delegated ./foo.js from dll-reference lib_3e48f809b016b57221ef ***!
  \**********************************************************************/
/***/ function(module, exports, __webpack_require__) {

module.exports = (__webpack_require__(1))(1);

/***/ },
/* 1 */
/* unknown exports provided */
/* all exports used */
/*!*******************************************!*\
  !*** external "lib_3e48f809b016b57221ef" ***!
  \*******************************************/
/***/ function(module, exports) {

module.exports = lib_3e48f809b016b57221ef;

/***/ },
/* 2 */,
/* 3 */
/* unknown exports provided */
/* all exports used */
/*!*****************!*\
  !*** ./app2.js ***!
  \*****************/
/***/ function(module, exports, __webpack_require__) {

var foo = __webpack_require__(/*! ./foo */ 0);

document.querySelector("#content").innerHTML += foo("app2", "App 2");


/***/ }
/******/ ]);

The lib.js file does indeed contain our common code. In contrast to the code splitting examples, the files created with the shared library plugins all contain a Webpack bootstrap loader. The main trick is seeing that there is a mapping of indirection for shared code like ./foo in our entry point code:

Let’s start at app1, index 2 (or app1:2):

var foo = __webpack_require__(/*! ./foo */ 0);

This means that we get foo from app1:0. Looking there we see:

module.exports = (__webpack_require__(1))(1);

this means we need to look at app1:1 to get a function, which we then call with the index 2. So we get a function from app1:2 for __webpack_require__(2):

module.exports = lib_3e48f809b016b57221ef;

which is the exported function of shared library. Then we call into lib:1 to find the actual code we want for foo.js:

module.exports = function (id, msg) {
  return "<h1 id=\"" + id + "\">" + msg + "</h1>";
};

So this is a bit of a tortured adventure of indirection, but hopefully it gets us closer to the big picture of how the code sharing works.

Once we build these files, we can load the common chunks and both apps with the following webpage:

index.html

<!DOCTYPE html>
<html>
  <body>
    <div id="content" />
    <script src="./dist/js/lib.js"></script>
    <script src="./dist/js/app1.js"></script>
    <script src="./dist/js/app2.js"></script>
  </body>
</html>
Advantages
  • Sharing across projects: The shared library (lib.js) can be reused across multiple projects / application servers to create 1+ uniform shared libraries. This should produce cache hits for the shared library even across totally separate applications.

  • Cache hits across deploys: Because the shared library is manually specified, it does not change without actually editing the source file. This means that you should get cache hits even across deploys of updates to the overall applications until the shared library source itself changes.

  • Faster entry point builds: You only need to build the shared library once. After you have the library and manifest, entry point builds should be faster in individual projects because shared parts are simply excluded from the build process.

Disadvantages
  • Inefficient Common Library: Because the shared code is manually specified there may be parts of the common library that are only used in one entry point or not at all over time. This means that you should regularly inspect and audit the shared library to make sure it includes the right "common" dependencies. It is a best practice to automate such introspection and review.

  • Mutiple build steps: You need at least two Webpack build steps instead of one for code splitting / normal builds.

  • Must manually lazy load: Unlike code splitting with require.ensure(), there is no automatic, Webpack-provided way to lazy load the shared code. However, this is easily done manually with a loader like: little-loader. For example, we could lazy load our entry points in the above example with something like lazy-load.html

    // Use little-loader to load `lib.js` first.
    window._lload("./dist/js/lib.js", function () {
      // Then load entry points in parallel
      // (assuming we don't care about order).
      window._lload("./dist/js/app1.js");
      window._lload("./dist/js/app2.js");
    });
    
  • Need caution with require.ensure() in shared bundle: Using require.ensure() / code splitting in the shared bundle may produce entry point references that assume a dependency is loaded when it is not. The easiest rule of thumb is to avoid require.ensure() in the dependencies in the shared bundle. (By contrast, code splitting / require.ensure() is totally fine in the application entry points.)

Previous GuideCode splitting
Next GuideTree shaking