Skip to content

Optimizing Frontend Builds: Shared Module Evaluation Order Solutions

Summary: Modern javascript build tools (such as esbuild, rollup) play a crucial role in optimizing code bundling and improving application performance. However, when handling shared code blocks (shared chunks), ensuring that module execution order complies with esm specification semantics while achieving efficient bundling is a complex technical challenge. This article delves into the root causes of this issue and provides a detailed analysis of two mainstream technical solutions and their inherent trade-offs.

Root Cause: Conflict Between ESM Semantics and Bundling Optimization

The esm specification defines a clear and strict order for module loading, linking, and evaluation. The execution of module top-level code, especially code containing side effects (e.g., modifying global state, performing I/O operations, depending on other modules' initialization), must follow dependency relationships precisely.

One of the core goals of build tools is to optimize the final output through code splitting and code reuse, reducing bundle size and network requests. When multiple entry points depend on the same shared module, bundlers typically prefer to extract these shared modules into separate chunks and include them as dependencies. However, different entry points may implicitly require the shared module to be initialized at different times or in different combinations with other modules.

For example:

js
import './lib2';
import './lib1';
if (window.lib !== 'lib1') throw 'fail';
js
import './lib1';
import './lib2';
if (window.lib !== 'lib2') throw 'fail';
js
window.lib = 'lib1';
js
window.lib = 'lib2';

The output after bundling with the above strategy:

js
import './lib1-4ba6e17e.js';
if (window.lib !== 'lib1') throw 'fail';
js
import './lib1-4ba6e17e.js';
if (window.lib !== 'lib2') throw 'fail';
js
window.lib = 'lib2';
window.lib = 'lib1';

For the two entry points main.js and main2.js, the timing of loading lib1.js and lib2.js is different, and thus their initialization timing is also different.

Therefore, under the esm specification, the optimization requirements for shared chunks sometimes conflict with esm's strict, deterministic execution order requirements, potentially leading to runtime errors or unexpected behavior.

Solution One: Semantic-Preserving Code Splitting

rollup author Lukas Taegert-Atkinson's idea:

I have an idea to fix this which would involve calculating the execution order for each entry point separately and then splitting into smaller chunks so that the relative execution order inside each chunk is correct for all entry points.

The core idea of this solution is to strictly follow module dependencies and execution timing requirements. Bundlers need to deeply analyze the module dependency graph and initialization requirements of shared modules from different entry points.

  • Mechanism:
    • Identify shared code that has different execution order dependencies due to different entry point import paths.
    • Perform more granular splitting of this shared code, generating independent or adapted chunk versions for different execution contexts, ensuring each entry point's execution flow complies with esm semantics.
  • Advantages:
    • High Fidelity: Maximally preserves the original module execution semantics of esm, improving code behavior predictability and correctness.
  • Challenges:
    • Increased Chunk Count: May result in generating numerous fine-grained chunk files.
    • Network Overhead: Significantly increases the number of http requests. Although http/2 and http/3 multiplexing alleviates some issues, the overhead of request establishment and header transmission still exists, potentially affecting loading performance, especially in poor network conditions.
    • Build Complexity: Places higher demands on bundlers' dependency analysis and code splitting logic, increasing build process complexity and time.
  • Potential Mitigation: Future javascript feature proposals, such as module declarations (formerly module fragments) or import attributes (formerly import assertions), may provide more flexible module loading control, theoretically helping optimize resource organization in such scenarios. However, these proposals are not yet standardized or widely implemented, and have browser compatibility issues.

Solution Two: Runtime-Delayed Initialization

esbuild author Evan Wallace's idea:

FWIW the other approach that I'm currently considering more strongly is to make code in shared chunks lazily-evaluated.

This solution takes a different approach: maintaining coarser-grained chunk division during bundling, and transforming shared module code that needs precise initialization timing control from immediate execution at declaration time to on-demand initialization at runtime.

  • Mechanism:

    • Distinguish between module member declaration and initialization.
    • Encapsulate initialization logic that was originally executed immediately at the module top level (such as class instantiation, function call assignments, dependencies on other modules' calculation results, etc.) into a specific initialization function (e.g., __init).
    • Insert runtime logic in the generated code to ensure the corresponding __init function is called before the module actually needs to execute its side effects or export initialized values.
    javascript
    // Original shared module (may have side effects or dependency order)
    export function utilityFn() {
      /* ... */
    }
    export class ComplexService {}
    export const configValue = expensiveCalculation(); // Immediate execution
    
    // Transformed shared module
    export function utilityFn() {
      /* ... */
    }
    export let ComplexService; // Declaration
    export let configValue; // Declaration
    
    let __initialized = false;
    function __init() {
      if (__initialized) return;
      __initialized = true;
      // eslint-disable-next-line no-class-assign
      ComplexService = class {}; // Delayed initialization
      // eslint-disable-next-line no-const-assign
      configValue = expensiveCalculation(); // Delayed initialization
    }
    
    // (Runtime code generated by Bundler will call __init() at appropriate times)
  • Advantages:

    • Bundling Efficiency: Maintains fewer chunk counts, effectively controlling the total number of http requests, typically more friendly to initial loading performance.
    • Compatibility: Primarily relies on existing javascript runtime behavior, not dependent on experimental browser features, better compatibility.
    • Semantic Simulation: Cleverly simulates the core semantics of esm module on-demand initialization through runtime mechanisms, solving execution order issues.
  • Considerations:

    • Minor Runtime Overhead: Introduces additional initialization function calls and state management logic, though performance impact is usually negligible.
    • Tree Shaking Interaction: Requires bundlers to be intelligent enough to correctly analyze the actual usage of code inside the __init function to achieve effective Tree Shaking.

This solution is similar to webpack's approach. The output after bundling the above example with webpack is as follows:

js
/******/ (() => {
  // webpackBootstrap
  /******/ 'use strict';
  /******/ var __webpack_modules__ = {
    /***/ 585: /***/ (
      __unused_webpack_module,
      __unused_webpack___webpack_exports__,
      __webpack_require__
    ) => {
      /* harmony import */ var _lib2__WEBPACK_IMPORTED_MODULE_0__ =
        __webpack_require__(155);
      /* harmony import */ var _lib2__WEBPACK_IMPORTED_MODULE_0___default =
        /*#__PURE__*/ __webpack_require__.n(
          _lib2__WEBPACK_IMPORTED_MODULE_0__
        );
      /* harmony import */ var _lib1__WEBPACK_IMPORTED_MODULE_1__ =
        __webpack_require__(54);
      /* harmony import */ var _lib1__WEBPACK_IMPORTED_MODULE_1___default =
        /*#__PURE__*/ __webpack_require__.n(
          _lib1__WEBPACK_IMPORTED_MODULE_1__
        );

      if (window.lib !== 'lib1') throw 'fail';

      /***/
    }

    /******/
  };
  /************************************************************************/
  /******/ // The module cache
  /******/ var __webpack_module_cache__ = {};
  /******/
  /******/ // The require function
  /******/ function __webpack_require__(moduleId) {
    /******/ // Check if module is in cache
    /******/ var cachedModule = __webpack_module_cache__[moduleId];
    /******/ if (cachedModule !== undefined) {
      /******/ return cachedModule.exports;
      /******/
    }
    /******/ // Create a new module (and put it into the cache)
    /******/ var module = (__webpack_module_cache__[moduleId] = {
      /******/ // no module.id needed
      /******/ // no module.loaded needed
      /******/ exports: {}
      /******/
    });
    /******/
    /******/ // Execute the module function
    /******/ __webpack_modules__[moduleId](
      module,
      module.exports,
      __webpack_require__
    );
    /******/
    /******/ // Return the exports of the module
    /******/ return module.exports;
    /******/
  }
  /******/
  /******/ // expose the modules object (__webpack_modules__)
  /******/ __webpack_require__.m = __webpack_modules__;
  /******/
  /************************************************************************/
  /******/ /* webpack/runtime/chunk loaded */
  /******/ (() => {
    /******/ var deferred = [];
    /******/ __webpack_require__.O = (result, chunkIds, fn, priority) => {
      /******/ if (chunkIds) {
        /******/ priority = priority || 0;
        /******/ for (
          var i = deferred.length;
          i > 0 && deferred[i - 1][2] > priority;
          i--
        )
          deferred[i] = deferred[i - 1];
        /******/ deferred[i] = [chunkIds, fn, priority];
        /******/ return;
        /******/
      }
      /******/ var notFulfilled = Infinity;
      /******/ for (var i = 0; i < deferred.length; i++) {
        /******/ var [chunkIds, fn, priority] = deferred[i];
        /******/ var fulfilled = true;
        /******/ for (var j = 0; j < chunkIds.length; j++) {
          /******/ if (
            (priority & (1 === 0) || notFulfilled >= priority) &&
            Object.keys(__webpack_require__.O).every(key =>
              __webpack_require__.O[key](chunkIds[j])
            )
          ) {
            /******/ chunkIds.splice(j--, 1);
            /******/
          } else {
            /******/ fulfilled = false;
            /******/ if (priority < notFulfilled) notFulfilled = priority;
            /******/
          }
          /******/
        }
        /******/ if (fulfilled) {
          /******/ deferred.splice(i--, 1);
          /******/ var r = fn();
          /******/ if (r !== undefined) result = r;
          /******/
        }
        /******/
      }
      /******/ return result;
      /******/
    };
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/compat get default export */
  /******/ (() => {
    /******/ // getDefaultExport function for compatibility with non-harmony modules
    /******/ __webpack_require__.n = module => {
      /******/ var getter =
        module && module.__esModule
          ? /******/ () => module['default']
          : /******/ () => module;
      /******/ __webpack_require__.d(getter, { a: getter });
      /******/ return getter;
      /******/
    };
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/define property getters */
  /******/ (() => {
    /******/ // define getter functions for harmony exports
    /******/ __webpack_require__.d = (exports, definition) => {
      /******/ for (var key in definition) {
        /******/ if (
          __webpack_require__.o(definition, key) &&
          !__webpack_require__.o(exports, key)
        ) {
          /******/ Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key]
          });
          /******/
        }
        /******/
      }
      /******/
    };
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/hasOwnProperty shorthand */
  /******/ (() => {
    /******/ __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/jsonp chunk loading */
  /******/ (() => {
    /******/ // no baseURI
    /******/
    /******/ // object to store loaded and loading chunks
    /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
    /******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
    /******/ var installedChunks = {
      /******/ 792: 0
      /******/
    };
    /******/
    /******/ // no chunk on demand loading
    /******/
    /******/ // no prefetching
    /******/
    /******/ // no preloaded
    /******/
    /******/ // no HMR
    /******/
    /******/ // no HMR manifest
    /******/
    /******/ __webpack_require__.O.j = chunkId =>
      installedChunks[chunkId] === 0;
    /******/
    /******/ // install a JSONP callback for chunk loading
    /******/ var webpackJsonpCallback = (
      parentChunkLoadingFunction,
      data
    ) => {
      /******/ var [chunkIds, moreModules, runtime] = data;
      /******/ // add "moreModules" to the modules object,
      /******/ // then flag all "chunkIds" as loaded and fire callback
      /******/ var moduleId,
        chunkId,
        i = 0;
      /******/ if (chunkIds.some(id => installedChunks[id] !== 0)) {
        /******/ for (moduleId in moreModules) {
          /******/ if (__webpack_require__.o(moreModules, moduleId)) {
            /******/ __webpack_require__.m[moduleId] =
              moreModules[moduleId];
            /******/
          }
          /******/
        }
        /******/ if (runtime) var result = runtime(__webpack_require__);
        /******/
      }
      /******/ if (parentChunkLoadingFunction)
        parentChunkLoadingFunction(data);
      /******/ for (; i < chunkIds.length; i++) {
        /******/ chunkId = chunkIds[i];
        /******/ if (
          __webpack_require__.o(installedChunks, chunkId) &&
          installedChunks[chunkId]
        ) {
          /******/ installedChunks[chunkId][0]();
          /******/
        }
        /******/ installedChunks[chunkId] = 0;
        /******/
      }
      /******/ return __webpack_require__.O(result);
      /******/
    };
    /******/
    /******/ var chunkLoadingGlobal = (self['webpackChunkwebpack_project'] =
      self['webpackChunkwebpack_project'] || []);
    /******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    /******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(
      null,
      chunkLoadingGlobal.push.bind(chunkLoadingGlobal)
    );
    /******/
  })();
  /******/
  /************************************************************************/
  /******/
  /******/ // startup
  /******/ // Load entry module and return exports
  /******/ // This entry module depends on other loaded chunks and execution need to be delayed
  /******/ var __webpack_exports__ = __webpack_require__.O(
    undefined,
    [758],
    () => __webpack_require__(585)
  );
  /******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
  /******/
  /******/
})();
js
/******/ (() => {
  // webpackBootstrap
  /******/ 'use strict';
  /******/ var __webpack_modules__ = {
    /***/ 246: /***/ (
      __unused_webpack_module,
      __unused_webpack___webpack_exports__,
      __webpack_require__
    ) => {
      /* harmony import */ var _lib1__WEBPACK_IMPORTED_MODULE_0__ =
        __webpack_require__(54);
      /* harmony import */ var _lib1__WEBPACK_IMPORTED_MODULE_0___default =
        /*#__PURE__*/ __webpack_require__.n(
          _lib1__WEBPACK_IMPORTED_MODULE_0__
        );
      /* harmony import */ var _lib2__WEBPACK_IMPORTED_MODULE_1__ =
        __webpack_require__(155);
      /* harmony import */ var _lib2__WEBPACK_IMPORTED_MODULE_1___default =
        /*#__PURE__*/ __webpack_require__.n(
          _lib2__WEBPACK_IMPORTED_MODULE_1__
        );

      if (window.lib !== 'lib2') throw 'fail';

      /***/
    }

    /******/
  };
  /************************************************************************/
  /******/ // The module cache
  /******/ var __webpack_module_cache__ = {};
  /******/
  /******/ // The require function
  /******/ function __webpack_require__(moduleId) {
    /******/ // Check if module is in cache
    /******/ var cachedModule = __webpack_module_cache__[moduleId];
    /******/ if (cachedModule !== undefined) {
      /******/ return cachedModule.exports;
      /******/
    }
    /******/ // Create a new module (and put it into the cache)
    /******/ var module = (__webpack_module_cache__[moduleId] = {
      /******/ // no module.id needed
      /******/ // no module.loaded needed
      /******/ exports: {}
      /******/
    });
    /******/
    /******/ // Execute the module function
    /******/ __webpack_modules__[moduleId](
      module,
      module.exports,
      __webpack_require__
    );
    /******/
    /******/ // Return the exports of the module
    /******/ return module.exports;
    /******/
  }
  /******/
  /******/ // expose the modules object (__webpack_modules__)
  /******/ __webpack_require__.m = __webpack_modules__;
  /******/
  /************************************************************************/
  /******/ /* webpack/runtime/chunk loaded */
  /******/ (() => {
    /******/ var deferred = [];
    /******/ __webpack_require__.O = (result, chunkIds, fn, priority) => {
      /******/ if (chunkIds) {
        /******/ priority = priority || 0;
        /******/ for (
          var i = deferred.length;
          i > 0 && deferred[i - 1][2] > priority;
          i--
        )
          deferred[i] = deferred[i - 1];
        /******/ deferred[i] = [chunkIds, fn, priority];
        /******/ return;
        /******/
      }
      /******/ var notFulfilled = Infinity;
      /******/ for (var i = 0; i < deferred.length; i++) {
        /******/ var [chunkIds, fn, priority] = deferred[i];
        /******/ var fulfilled = true;
        /******/ for (var j = 0; j < chunkIds.length; j++) {
          /******/ if (
            (priority & (1 === 0) || notFulfilled >= priority) &&
            Object.keys(__webpack_require__.O).every(key =>
              __webpack_require__.O[key](chunkIds[j])
            )
          ) {
            /******/ chunkIds.splice(j--, 1);
            /******/
          } else {
            /******/ fulfilled = false;
            /******/ if (priority < notFulfilled) notFulfilled = priority;
            /******/
          }
          /******/
        }
        /******/ if (fulfilled) {
          /******/ deferred.splice(i--, 1);
          /******/ var r = fn();
          /******/ if (r !== undefined) result = r;
          /******/
        }
        /******/
      }
      /******/ return result;
      /******/
    };
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/compat get default export */
  /******/ (() => {
    /******/ // getDefaultExport function for compatibility with non-harmony modules
    /******/ __webpack_require__.n = module => {
      /******/ var getter =
        module && module.__esModule
          ? /******/ () => module['default']
          : /******/ () => module;
      /******/ __webpack_require__.d(getter, { a: getter });
      /******/ return getter;
      /******/
    };
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/define property getters */
  /******/ (() => {
    /******/ // define getter functions for harmony exports
    /******/ __webpack_require__.d = (exports, definition) => {
      /******/ for (var key in definition) {
        /******/ if (
          __webpack_require__.o(definition, key) &&
          !__webpack_require__.o(exports, key)
        ) {
          /******/ Object.defineProperty(exports, key, {
            enumerable: true,
            get: definition[key]
          });
          /******/
        }
        /******/
      }
      /******/
    };
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/hasOwnProperty shorthand */
  /******/ (() => {
    /******/ __webpack_require__.o = (obj, prop) =>
      Object.prototype.hasOwnProperty.call(obj, prop);
    /******/
  })();
  /******/
  /******/ /* webpack/runtime/jsonp chunk loading */
  /******/ (() => {
    /******/ // no baseURI
    /******/
    /******/ // object to store loaded and loading chunks
    /******/ // undefined = chunk not loaded, null = chunk preloaded/prefetched
    /******/ // [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
    /******/ var installedChunks = {
      /******/ 889: 0
      /******/
    };
    /******/
    /******/ // no chunk on demand loading
    /******/
    /******/ // no prefetching
    /******/
    /******/ // no preloaded
    /******/
    /******/ // no HMR
    /******/
    /******/ // no HMR manifest
    /******/
    /******/ __webpack_require__.O.j = chunkId =>
      installedChunks[chunkId] === 0;
    /******/
    /******/ // install a JSONP callback for chunk loading
    /******/ var webpackJsonpCallback = (
      parentChunkLoadingFunction,
      data
    ) => {
      /******/ var [chunkIds, moreModules, runtime] = data;
      /******/ // add "moreModules" to the modules object,
      /******/ // then flag all "chunkIds" as loaded and fire callback
      /******/ var moduleId,
        chunkId,
        i = 0;
      /******/ if (chunkIds.some(id => installedChunks[id] !== 0)) {
        /******/ for (moduleId in moreModules) {
          /******/ if (__webpack_require__.o(moreModules, moduleId)) {
            /******/ __webpack_require__.m[moduleId] =
              moreModules[moduleId];
            /******/
          }
          /******/
        }
        /******/ if (runtime) var result = runtime(__webpack_require__);
        /******/
      }
      /******/ if (parentChunkLoadingFunction)
        parentChunkLoadingFunction(data);
      /******/ for (; i < chunkIds.length; i++) {
        /******/ chunkId = chunkIds[i];
        /******/ if (
          __webpack_require__.o(installedChunks, chunkId) &&
          installedChunks[chunkId]
        ) {
          /******/ installedChunks[chunkId][0]();
          /******/
        }
        /******/ installedChunks[chunkId] = 0;
        /******/
      }
      /******/ return __webpack_require__.O(result);
      /******/
    };
    /******/
    /******/ var chunkLoadingGlobal = (self['webpackChunkwebpack_project'] =
      self['webpackChunkwebpack_project'] || []);
    /******/ chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
    /******/ chunkLoadingGlobal.push = webpackJsonpCallback.bind(
      null,
      chunkLoadingGlobal.push.bind(chunkLoadingGlobal)
    );
    /******/
  })();
  /******/
  /************************************************************************/
  /******/
  /******/ // startup
  /******/ // Load entry module and return exports
  /******/ // This entry module depends on other loaded chunks and execution need to be delayed
  /******/ var __webpack_exports__ = __webpack_require__.O(
    undefined,
    [758],
    () => __webpack_require__(246)
  );
  /******/ __webpack_exports__ = __webpack_require__.O(__webpack_exports__);
  /******/
  /******/
})();
js
(self['webpackChunkwebpack_project'] =
  self['webpackChunkwebpack_project'] || []).push([
  [758],
  {
    /***/ 54: /***/ () => {
      window.lib = 'lib1';

      /***/
    },

    /***/ 155: /***/ () => {
      window.lib = 'lib2';

      /***/
    }
  }
]);

It can be observed that webpack's generated shared chunks adopts the delayed initialization approach, using the webpack runtime to manage the initialization timing of modules in shared chunks.

Trade-offs and Decisions: Accuracy vs. Efficiency

The two solutions represent different engineering philosophies in handling shared module execution order issues:

  • Solution One prioritizes precise ESM semantics, at the cost of potentially sacrificing some bundling efficiency and loading performance (especially in terms of request count).
  • Solution Two prioritizes bundling efficiency and compatibility, satisfying semantic requirements through runtime simulation, potentially introducing minor runtime overhead.

Bundler Implementation and Developer Considerations

Different build tools may adopt different default strategies or provide configuration options to let developers influence this behavior:

  • rollup and its ecosystem (such as vite) are known for their strict adherence to the esm specification and fine-grained code splitting capabilities, their strategy sometimes closer to Solution One's approach.
  • esbuild focuses primarily on extremely high build speed, its optimization strategy may lean more towards Solution Two's efficiency in some default configurations, but continues to improve handling of complex esm scenarios.

When facing specific projects, developers need to make judgments and configurations based on the following factors:

  • Project sensitivity to side effects and execution order: Are there shared modules that strongly depend on specific initialization order?
  • Performance bottleneck analysis: Is the main performance bottleneck of the application network request count or runtime computation?
  • Target environment compatibility: Is there a need to support older environments that don't support the latest javascript features?
  • Build tool configuration capabilities: Understand and utilize relevant configuration items provided by build tools (such as rollup/vite's output.manualChunks or specific plugins) to fine-tune chunking and initialization strategies.

Conclusion

The execution order issue of shared modules is a typical challenge in modern frontend build optimization, reflecting the ongoing balance between adhering to standard semantics and pursuing engineering efficiency. The final choice is often a pragmatic trade-off based on specific project requirements and constraints.

Contributors

Changelog

Discuss

Released under the CC BY-SA 4.0 License. (dbcbf17)