Optimizing Frontend Builds: Shared Module Evaluation Order Solutions
Related Material
Module execution order can be incorrect - Nov 30, 2020
Incorrect import order with code splitting and multiple entry points - Sep 21, 2020
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:
import './lib2';
import './lib1';
if (window.lib !== 'lib1') throw 'fail';
import './lib1';
import './lib2';
if (window.lib !== 'lib2') throw 'fail';
window.lib = 'lib1';
window.lib = 'lib2';
The output after bundling with the above strategy:
import './lib1-4ba6e17e.js';
if (window.lib !== 'lib1') throw 'fail';
import './lib1-4ba6e17e.js';
if (window.lib !== 'lib2') throw 'fail';
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 withesm
semantics.
- Advantages:
- High Fidelity: Maximally preserves the original module execution semantics of
esm
, improving code behavior predictability and correctness.
- High Fidelity: Maximally preserves the original module execution semantics of
- Challenges:
- Increased Chunk Count: May result in generating numerous fine-grained
chunk
files. - Network Overhead: Significantly increases the number of
http
requests. Althoughhttp/2
andhttp/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.
- Increased Chunk Count: May result in generating numerous fine-grained
- Potential Mitigation: Future
javascript
feature proposals, such asmodule declarations
(formerlymodule fragments
) orimport attributes
(formerlyimport 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 ofhttp
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.
- Bundling Efficiency: Maintains fewer
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 effectiveTree Shaking
.
This solution is similar to webpack
's approach. The output after bundling the above example with webpack
is as follows:
/******/ (() => {
// 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__);
/******/
/******/
})();
/******/ (() => {
// 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__);
/******/
/******/
})();
(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 theesm
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
'soutput.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.