Is Webpack Packaging Too Slow? Try the Bundleless Mode
Webpack can package various resources into bundles. As increasing resources need to be packaged, the efficiency of the packaging process deteriorates. What if we skip the packaging of resources? Can we achieve qualitative improvement by having the browser directly load the resources? This article introduces the ideas, core technologies, and Vite implementations to achieve Bundleless local development based on the browser’s ESModule. It also describes the practices in the Alibaba supply chain point of sale (POS) scenario.
Webpack was originally designed to solve issues pertaining to frontend modularization and the Node.js ecosystem. Over the past eight years, webpack has become increasingly powerful.
However, due to the additional packaging process, the building speed is getting slower as the project grows. As a result, each startup takes dozens of seconds (or minutes), followed by a round of build optimization. As the project grows, the build speed deteriorates again, and another round of optimization is required, leading to an optimization loop.
When the project reaches a certain scale, the benefits of bundle-based building optimization fade and cannot be qualitatively improved. From another perspective, the main reason for an inefficient webpack is that it packages various resources into bundles. We may get rid of the loop and achieve qualitative improvement by having the browser load the corresponding resources without packaging.
In the Bundleless architecture, we don’t need to build a complete bundle. When a file is modified, the browser only needs to reload the file. We can achieve the following goals by removing the building process:
- Extremely Fast Local Startup: You only need to start the local service
- Extremely Quick Code Compilation: Only a single file needs to be processed at a time
- The time complexity of project development and building is always O(1), enabling continuously efficient project building.
- Simpler Debugging Experience: SourceMap is no longer a strong dependency for achieving stable single-file debugging.
Based on the preceding possibilities, the Bundleless architecture will redefine the local development of the frontend. This allows us to regain the experience we had ten years ago. The frontend modifies a single file and only needs to refresh the file for the modification to take effect. In addition, the HotModuleReplace technology of the frontend allows the modification to take effect immediately by saving the file without the refreshing operation.
Dynamic module loading is an important basic capability for implementing the Bundleless architecture. It can be achieved in the following ways:
- Using ES module loaders, such as System.js, which features high compatibility
- Using the web-standard ESModule, which is future-proof and simplifies the overall architecture
Compatibility has little impact on the local development process. At the same time, ESModule has been embedded in more than 90% of browsers. Therefore, we can use ESModule to have browsers load the required modules, achieving the low-cost and future-proof Bundleless architecture.
In the past two years, many ESModule-based development tools, such as Vite, Snowpack, and es-dev-server, emerged in the community. This article introduces the ideas, core technologies, and Vite implementations to achieve Bundleless local development based on the browser’s ESModule. It also describes the practices in the Alibaba supply chain POS scenario.
2. Differences between the Bundle and Bundleless Modes from the Resource Loading Perspective
The following section uses the popular default project create-react-app as an example to show the differences between the Bundle and Bundleless modes in the loading of page rendering resources.
The Webpack-Based Bundle Mode
The following figure shows the module loading mechanism:
Both project startup and file modification involve the packaging operation, adding to the overall time consumption.
The ESModule-Based Bundleless Mode
As you can see from the preceding figure, no bundle or chunk file is built. Instead, the corresponding local files are loaded directly.
As shown in the preceding figure, in the Bundleless mechanism, project startup only involves starting a server to handle requests from the browser. In addition, when a file is modified, only this file needs to be processed while the other files can be directly read from the cache.
The Bundle ModeThe Bundleless ModeProject StartupGo through the full packaging processStart devServerBrowser LoadingWait for the packaging to complete and load the corresponding bundleInitiate a request and map it to a local fileLocal File UpdateRepackage into a bundleRequest a single file again
The Bundleless mode can make full use of the dynamic loading features of the browser and skip the packaging process. As a result, we can achieve an extremely fast project startup and only need to recompile a single file for a local update. The following section describes how to implement the Bundleless mode based on the browser’s ESModule.
3. Implementing the Bundleless Mode
Loading Modules Using ESModule
The first step for implementing the Bundleless mode is to have the browser load the corresponding modules.
Set Type=”module” to Enable ESModule
// Enable ESModule by setting type="module" in the script tag.
import React from 'https://cdn.pika.dev/react'
import ReactDOM from 'https://cdn.pika.dev/react-dom' ReactDOM.render('Hello World', document.getElementById('root'))
Use Import-Maps to Support Bare Import
When using the import-maps standard that has been implemented for Chrome, you can implement bare import support by running the “import React from ‘react’” statement. In the future, we can use this capability to implement online Bundleless deployment.
<! -- Enable chrome://flags/#enable-experimental-productivity-features -->
// Support bare import.
import React from 'react'
import ReactDOM from 'react-dom' ReactDOM.render('Hello World!', document.getElementById('root'))
So far, you know how to use the native ESModule of the browser. For the local development scenario, we only need to start a local devServer to handle the browser’s requests and map them to the corresponding local files. Meanwhile, we can point the resource import path in the project to a local path so the browser can directly load the local files. For example, you can use the following code to directly point the entry JS file to the local path, and then intercept the corresponding request by the devServer to return the corresponding file.
<! -- Directly point to the local path -->
<script type="module" src="/src/main.jsx"></script>
Loading Non-JS File Resources
import React from 'react'
import ReactDOM from 'react-dom'
Import'./index.css'// Import the CSS file.
import App from '. /App' //Import the JSX file.// Use the JSX syntax.
ReactDOM.render(<App />, document.getElementById('root'))
The browser processes files based on Content-Type, but does not care about the specific file type. Therefore, we need to convert the corresponding resources into the ESModule format when the browser initiates a request. In addition, we need to set the corresponding Content-Type to JS and return it to the browser for execution. Then, the browser will parse the data based on the JS syntax. The overall process is shown in the following figure:
The following figure shows the implementation of Vite, which processes different files dynamically when returning a response.
HotModuleReplace can be used for the modified code to take effect immediately without refreshing the page. It allows the modified code to take effect with almost zero delays once being saved in conjunction with the Bundleless mode that delivers extremely fast and effective speeds. Currently, React can only be implemented using react-hot-loader in the webpack scenario. However, this implementation is defective in certain scenarios. We also recommend migrating to react-refresh implemented by the React team, even though react-refresh has not been implemented in webpack. In the Bundleless scenario, each of our components is loaded independently, and therefore react-refresh needs to be integrated. To complete this integration, we only need to add the corresponding scripts at the beginning and end of the file when returning a response to the browser.
The full implementation of HotModuleReplace is more complex than what is shown in the preceding figure. In addition, it requires an analysis mechanism to determine which files need to be replaced when a file is changed and whether reloading is required. In the Bundleless scenario, repackaging into a complete bundle is no longer required, and single files can be modified more flexibly. Therefore, the related implementations are easier.
The following figure shows the relevant implementations in Vite:
Optimizing Slow Page Loading Due to a Large Number of Requests
The Bundleless mode removes the need of packaging and improves the startup speed. However, for some modules with a large number of external dependencies or files, many requests are required to obtain all resources, resulting in an increased page loading time during development. For example, the “import lodash-es” command initiates a large number of requests in the browser, as shown in the following figure:
To solve this issue, we can package external dependencies into a single file to reduce the number of network requests initiated due to excessive external dependencies.
In the startup process of Vite, the “vite optimize” process automatically packages the dependencies in package.json into an ES6 module using Rollup.
Pre-packaging improves the page loading speed. In addition, when using @rollup/plugin-commonjs, we can package external dependencies of commonjs into the ESModule format, expanding the application scope of the Bundleless mode.
4. Practices in the Supply Chain POS Scenario
The supply chain POS business (handled by our team) can be divided into the home improvement industry for building materials and home furnishing, and the retail industry for offline stores. In the technical architecture, each domain bundle is separately developed and eventually merged into a large single page application (SPA) using the underlying SDK. Due to the complexity of the project, the daily development process involves the following pain points:
- The startup and time consumption of the project are relatively long.
- Secondary compilation takes a long time after a change is made.
- Hot module replacement (HMR) capabilities are not stable, and repeated scenario construction is required during development.
- Debugging depends on SourceMap capabilities and is occasionally fluctuant.
Bearing the preceding issue in mind and leveraging the implementations of Vite, we have tried and implemented the Bundleless mode in the local development environment, greatly improving the local development experience in some experimental projects.
The Bundleless mode dramatically improves startup and modification efficiency.
So far, we have achieved the software development and building speed at a single-bundle dimension.
As you can see, webpack takes about ten seconds to start a single bundle, whereas Bundleless-based Vite takes only about one second, which is a tenfold improvement.
On the other hand, the overall page loading time is about four seconds, which is still shorter than the building time of webpack. In addition, you can see from the preceding video that the HMR response speed reaches milliseconds, realizing instant effectiveness upon saving.
Debug a Single File without SourceMap
Problems in the Implementation Process and Solutions
During the implementation process, the main problems lie with the relevant modules. They do not conform to ESModule specifications and the following coding standardization issues:
- Some modules are packaged without ESModule included.
- The coding of the Less plug-in dependency on node_modules is not standardized.
- No specification is defined for JSX file extensions.
- No specification is defined for babel-runtime processing.
Some Modules Are Packaged without the ESModule
- For output packages that do not contain the ESModule or are output incorrectly, different policies apply to different types of packages.
- Internal Packages: Release new versions of packages with ESModule included by upgrading webpack-scaffold
- External Dependencies: Upgrade modules, such as number-precision, using the issue, pull request, and other methods
- For packages that cannot include ESModule due to historical reasons, use @ rollup/plugin-commonjs to convert them to the ESModule format
Coding of the Less Dependency on node_modules
//~ Supported only in the webpack less-loader, not in the native Less plug-in// Uniformly migrate to the following mode.
@import '@ali/pos-style-mixin/style/lst.less';//Configure lessOptions for final packaging in the original webpack less-loader.
paths: [path.resolve(cwd, 'node_modules')],
The Specification for JSX File Extensions
Vite compiles files based on their file extensions. In webpack, JSX, JS, and other similar files are processed by babel-loader. As a result, some files that are essentially JSX files but without the JSX extension are ignored. Vite performs esbuild compilation only on
/.(tsx? |jsx)$/ files. For JS files, it skips the esbuild process. We must convert the ignored files to JSX files to correct this issue.
Processing of Babel-Runtime
After you use babel-plugin-transform-runtime, the output of packaging looks like this:
As you can see, the referenced @babel/runtime/helpers/extends is in the commonjs format and cannot be directly used. Use any of the following solutions to solve this issue:
1. For internally packaged modules, add the useModules configuration when performing es6 packaging. This way, the resulting code directly references @babel/runtime/helpers/esm/extends.
2. For modules with high repackaging cost, replace @babel/runtime/helpers with @babel/runtime/helpers/esm during runtime by using the plug-in mechanism of Vite. To implement the replacement, you can configure an alias.
So far, we have gone through the major problems in the migration process in the Vite development environment and their solutions. The extended implementation of the Bundleless mode is in progress. The Bundleless mode is implemented not only to adapt to the Vite development model but also to standardize the coding of each module in the future. We will standardize our modules with ESModule to implement the Bundleless mode at lower costs when new tools and ideas emerge.
5. Feasibility of Using the Bundleless Mode for Deployment
Due to the limitations on network requests and browser parsing, the Bundle mode can still provide great benefits to large applications in terms of the loading speed. In 2018, the V8 engine also recommended using the Bundleless mode in local development and small web applications. As the browser and network performance continue to increase and the caching capabilities of ServiceWorker grow, the impact of network loading is becoming less. In scenarios where compatibility issues can be ignored, you can try to deploy code loaded through ESModule.
This article shares some ideas about how to implement the Bundleless architecture to improve frontend development efficiency. It also describes some implementation practices in specific business scenarios. The essence of using the Bundleless architecture is to hand the parsing job in webpack over to the browser, which minimizes code conversion and accelerates building during development. The Bundleless mode also allows better use of browser-related development tools.
Last but not least, thanks to the outstanding standards and tools such as ESModule, Vite, and Snowpack, the frontend development experience has taken a substantial step forward.