Keep Webpack Fast: A tried and tested guide for better build performance

Mayank Shekhar
7 min readSep 14, 2023

--

Photo by Caleb Woods on Unsplash

Webpack is a powerful asset bundling tool for frontend development. However, as projects grow in complexity, its all-encompassing approach and the vast array of third-party tools available can present challenges when it comes to optimizing performance. It’s not uncommon for projects to suffer from suboptimal performance rather than exceptional speed. Fortunately, there’s a way to overcome this hurdle. After extensive research, numerous trials, and inevitable errors, we’ve distilled our findings into a practical guide. This guide aims to share the insights we gained during our journey toward achieving faster build times.

The Problem

In modern web applications, optimizing bundle size is crucial for improving application performance and user experience but many optimizations cause our application to grow in size.

The bigger your Javascript application becomes the longer it takes for webpack to build a bundle. No surprises here. Even webpack caching does not help.

Before you begin, measure

Before diving into optimization efforts, it’s essential to gain insight into where the time is being consumed. While webpack might not readily provide this information, alternative methods can help you obtain the necessary data.

How do you measure?

The node inspector

Node includes a built-in inspector that is helpful for profiling builds. For those who are new to performance profiling, don’t be discouraged, Google has provided comprehensive guidance on this topic. It’s valuable to have a basic grasp of the different phases involved in a webpack build. While webpack’s documentation briefly touches on this, you may find it equally effective to review some of the core code.

Keep in mind that for particularly large builds (think hundreds of modules or lasting longer than a minute), it may be necessary to segment your profiling sessions to prevent your developer tools from crashing.

Logging

Profiling proved invaluable in pinpointing the bottlenecks in our build process initially. However, it fell short in tracking performance trends over time. Our objective was to have each build report detailed timing data, allowing us to monitor how much time was allocated to each resource-intensive step such as transpilation and minification. This information was crucial in assessing the effectiveness of our optimizations.

The majority of our optimization work was not within webpack itself but rather involved numerous loaders and plugins we relied on. Unfortunately, most of these dependencies did not offer granular timing data. While we would have preferred a standardized mechanism for third-party reporting of such information within webpack, we had to resort to custom logging in the interim.

With loaders, this involved creating modified versions of our dependencies. While this approach may not be sustainable in the long run, it served as a valuable tool for identifying sluggishness as we continued to work on optimization. In contrast, profiling plugins proved to be a more straightforward task.

The value of this information vastly outweighs the nuisance of getting it, and once you understand where the time is spent you can work to reduce it effectively.

Possible Solutions

Source maps

Perfect SourceMaps are slow.

devtool: "source-map" cannot cache SourceMaps for modules and need to regenerate complete SourceMap for the chunk. It's something for production.

devtool: "eval-source-map" is really as good as devtool: "source-map", but can cache SourceMaps for modules. It's much faster for rebuilds.

devtool: "eval-cheap-module-source-map" offers SourceMaps that only maps lines (no column mappings) and are much faster.

devtool: "eval-cheap-source-map" is similar but doesn't generate SourceMaps for modules (i.e., jsx to js mappings).

devtool: "eval" has the best performance, but it only maps to compiled source code per module. In many cases this is good enough. (Hint: combine it with output.pathinfo: true.)

The UglifyJsPlugin uses SourceMaps to map errors to source code. And SourceMaps are slow. As you should only use this in production, this is fine. If your production build is really slow (or doesn’t finish at all) you can disable it with new UglifyJsPlugin({ sourceMap: false }).

Use recordsPath option

Webpack assigns a unique identifier to each module in your dependency hierarchy. Whenever new modules are introduced or existing ones are removed, the hierarchy evolves, causing the IDs of these modules to change accordingly. These identifiers are embedded into every file generated by webpack. Excessive changes in module structure can lead to unwarranted rebuilding. To address this issue, you can employ records to maintain consistency in module IDs across builds, thus preventing unnecessary recompilations.

Sharing Code

It’s common for identical code to be included in multiple bundles, which can lead to unnecessary duplication of work for the minifier. To address this issue, we meticulously examined our bundles using tools like the webpack Bundle Analyzer and Bundle Buddy. This scrutiny allowed us to identify duplicate code segments and extract them into shared chunks using webpack’s CommonsChunkPlugin or SplitChunksPlugin.

Parallelize

Many of the tasks performed by webpack are inherently suitable for parallel processing. Significant performance improvements can be achieved by distributing the workload across as many processor cores as available. If you have surplus CPU cores at your disposal, this is the opportune moment to harness their power.

Turning on the parallel flag, In webpack 5, this setting is available in other options by the parallelism .

Skip parsing

Webpack meticulously analyzes every JavaScript file it encounters, building a syntax tree to identify dependencies. This operation consumes significant resources. However, if you are confident that a particular file or group of files will never have import, require, or define statements, you have the option to instruct webpack to skip them during this parsing process. This exclusion, especially for large libraries, can lead to substantial performance enhancements. You can find further information on how to achieve this by referring to the ‘noParse’ option.

DLL Plugin

The DllPlugin allows you to extract prebuilt bundles, which can be used by webpack in a subsequent phase. It is particularly effective for handling substantial, infrequently updated dependencies such as vendor libraries.

Create a manifest chunk

We implement hashed filenames to facilitate cache busting with each new version release. When you inspect the Network tab in your browser’s developer tools, you’ll observe requests for files with names like “application.d4782516387512213.min.js.” While this approach effectively manages caching, it also poses a challenge for webpack in mapping modules to their corresponding filenames without the assistance of a unique identifier.

To address this issue, we employ a digest, which is essentially a straightforward mapping of module IDs to hashes. Webpack relies on this digest to resolve filenames when importing modules asynchronously.

Maintaining the stability of module IDs was just the first step. Our objective was to separate the module digest into an entirely distinct file — one that could undergo regular updates without incurring the cost of rebuilding and re-downloading for both us and our customers. To achieve this, we introduced a manifest file using the SplitChunks plugin. This approach significantly decreased the frequency of rebuilds and had the additional benefit of enabling us to ship only a single instance of webpack’s boilerplate code. You can find more detailed info in this article.

Exclusions

Likewise, you have the ability to omit specific files from loaders, and numerous plugins provide comparable configuration choices. This practice can significantly enhance the performance of tools such as transpilers and minifiers, which also depend on syntax trees for their specialized operations. You should adopt an approach which involves exclusively transpiling code that you are certain will make use of ES6 features, and you should entirely bypass the minification process for non-customer facing code.

Caching

Our deployment process is fast-paced, resulting in minimal discrepancies between the current build and its predecessors. By strategically implementing caching, we were able to bypass much of the processing that webpack would otherwise perform.

Our caching strategy involves the use of babel-loader, which has the option to utilize its built-in caching feature. Caching methods like HardSourceWebpackPlugin and cache-loader are great for local development, and can cut off large amounts of time.

They also have an initial overhead, however. Dropping caching in your production config can save you this overhead.

Stay up-to-date

In the webpack ecosystem, staying current with updates pays dividends. The core team has been diligently enhancing build speed in recent times, and failing to use the latest versions of your dependencies might mean missing out on performance enhancements. During our transition from webpack 4.0 to 5.0, we witnessed a significant reduction in build times by several seconds, all without altering our configuration. These improvements continue to roll in.

It’s crucial to stay up-to-date and be aware of new features, such as the parallelism mentioned earlier. At Adobe, we actively monitor Github for releases, endeavor to contribute when possible, and closely follow the impressive work of webpack, babel, and other key players who share their insights through various forms of media.

Additionally, it’s essential not to overlook the importance of keeping your Node.js version current. Improvements in performance aren’t limited to just package updates; maintaining an up-to-date Node version also plays a critical role.

Conclusion

Webpack is an exceptional and adaptable tool that doesn’t have to be resource-intensive. Implementing these strategies has allowed us to slash our average build time by more than 70%. While these changes have greatly enhanced our engineers’ deployment experience, we acknowledge that there’s still room for improvement. If you have any insights or ideas to further boost build performance, we eagerly welcome your input.

Moreover, Infrastructure-level projects like webpack can be surprisingly under-funded; whether it’s with time or with money, contributing to the tools you use will do much to improve the ecosystem for you and everyone else in the community.

Further Reading

#build-performance #es6 #frontend #javascript #webpack

--

--

Mayank Shekhar

Senior Software Engineer at Adobe Systems. Prev: SSWE @Tata 1mg | Helping people learn frontend development.