The full text is 2500 words and takes about 20 minutes to read.

What is Esbuild

Esbuild is a very new module packaging tool that offers similar resource packaging capabilities to tools like Webpack, Rollup, Parcel, etc., but with ridiculously high performance advantages:

From top to bottom, the time difference is hundreds of times. This huge performance advantage has made Esbuild popular among node-based build tools, especially after Vite 2.0 announced the use of Esbuild pre-build dependencies.

So the question is, how does it work? After looking through a lot of materials, I came up with some key factors:

Let’s expand on that.

Why do fast


Language advantage

While most front-end packaging tools are implemented in JavaScript, Esbuild is written in Go. Both languages have their own strengths, but in cpu-intensive scenarios like resource packaging, Go has a performance advantage. How big is the difference? For example, calculate the Fibonacci sequence 50 times.

function fibonacci(num) {
    if (num < 2) {
        return 1
    }
    return fibonacci(num - 1) + fibonacci(num - 2)} (() = > {
    let cursor = 0;
    while (cursor < 50) {
        fibonacci(cursor++)
    }
})()
Copy the code

Go version:

package main

func fibonacci(num int) int{
    if num<2{
        return 1
    }
    
    return fibonacci(num- 1) + fibonacci(num2 -)}func main(a){
    for i := 0; i<50; i++{
        fibonacci(i)
    }
}
Copy the code

The execution time of the JavaScript version is about 332.58s, and the execution time of the Go version is about 147.08s, which is about 1.25 times of the difference. This simple experiment cannot accurately quantify the performance difference between the two languages. However, it is still obvious that Go performs better in CPU-intensive scenarios.

In the end, although modern JS engines have greatly improved compared to 10 years ago, JavaScript is still an interpreted language in nature. Every time a JavaScript program is executed, the interpreter needs to translate the source code into machine language while scheduling execution. Go, on the other hand, is a compiled language that translates the source code into machine code at compile time and only needs to execute the machine code directly at startup. This means that programs written in Go have one less dynamic interpretation process than JavaScript.

This language-level difference is particularly noticeable in the packaging scenario, where Esbuild parses user code while the JavaScript runtime interprets it; By the time the JavaScript runtime has interpreted the code and is ready to start, Esbuild may have been wrapped up and exited the process!

Therefore, at the level of compilation and operation, Go prefixes the source code compilation process, which has higher performance compared to the way of JavaScript explaining and running.

Multithreading advantage

Go has the ability to run multithreading by nature, while JavaScript is a single-threaded language by nature. It was not possible to implement multithreading in browsers and Nodes until the introduction of WebWorker specifications.

I have studied the Rollup and Webpack codes, and to the best of my knowledge neither utilizes the multithreading capabilities WebWorker provides. In contrast, Esbuild’s core selling point is performance. Its implementation algorithm has been carefully designed to saturate each CPU core as much as possible. In particular, the parsing and code generation stages of the packaging process have been fully parallel processing.

In addition to parallelism at the CPU instruction execution level, Go threads can share the same memory space, whereas JavaScript threads have their own memory heap. This means that multiple processing units in Go, such as the thread parsing resource A, can read the results of resource B directly, whereas in JavaScript the same operation requires calling the communication interface woker.postMessage to copy data between threads.

So at the runtime level, Go has natural multithreading capabilities and more efficient memory usage, which means higher runtime performance.

moderation

Yes, that’s right, abstinence!

Esbuild is not just another Webpack, it just provides the minimum set of features needed to build a modern Web application, and it will not add the kinds of build features we are familiar with on a large scale in the future. The main features of the latest version of Esbuild are:

  • Supports JS, TS, JSX, CSS, JSON, text, and images
  • Incremental updating
  • Sourcemap
  • Development server Support
  • Code compression
  • Code split
  • Tree shaking
  • Plug-in support

As you can see, the resource types and engineering features supported in this list are very few and may not even be sufficient to support the development requirements of a large project. Beyond that, the website clearly states that there are no plans to support the following features in the future:

  • Elm, Svelte, Vue, Angular code file formats
  • Ts Type Check
  • AST related operation apis
  • Hot Module Replace
  • Module Federation

Moreover, the plugin system designed by Esbuild is not intended to cover these scenarios, which means that third-party developers cannot implement these functions in a non-intrusive way through plug-ins. Emmm, as you can see, is likely to see a lot of tweaks in the future.

Esbuild solves only part of the problem, so its architecture complexity is relatively low, its coding complexity is relatively low, and it is easier to achieve maximum performance than Webpack, Rollup and other monolithic tools. Another benefit of restrained functional design is a variety of additional tools that are completely customized for performance.

custom

To recap, in tools like Webpack and Rollup, we had to use a lot of additional third-party plug-ins to address various engineering needs, such as:

  • Use Babel for ES version translation
  • Use ESLint for code checking
  • TSC is used to achieve TS code translation and code inspection
  • Use CSS preprocessing tools such as LESS, stylus, and SASS

We’re so used to it that it’s just the way things are supposed to be, most of us probably don’t even realize there’s another way things can work. Esbuild gets a head start, complete selection! Completely rewrite all the tools needed for the entire compilation process! This means that it needs to rewrite the loading, parsing, linking, and code generation logic of js, TS, JSX, JSON, and other resource files.

Development costs are high, and there is a risk of being locked in, but the benefits are huge. It can follow the principle of customizing all stages of compilation with performance as the highest priority, for example:

  • Rewrite the TS translation tool, completely abandon TS type checking, only code conversion
  • Most packaging tools disassemble lexical analysis, syntax analysis, symbol declaration and other steps into multiple processing units with high cohesion and low coupling. Each module has clear responsibilities, high readability and maintainability. Esbuild adheres to the principle of performance first and uses a counterintuitive design pattern to blend multiple processing algorithms together to reduce the performance loss caused by data flow during compilation
  • Consistent data structures, and the resulting efficient caching strategies, are covered in the next section

On the one hand, this deep customization reduces the design cost and keeps the architecture consistency of the compilation chain. On the one hand, it can implement the principle of performance first to ensure the optimal performance of each link and interaction between links. With the sacrifice of functionality, readability, and maintainability, the compilation performance is almost perfect.

Structural consistency

In the previous section, we saw that Esbuild chose to override translation tools including JS, TS, JSX, CSS, etc., so it is more likely to ensure structural consistency of source code between compilation steps. For example, when using Babel-Loader to process JavaScript code in Webpack, Multiple data conversions may be required:

  • Webpack reads the source code as a string
  • Babel parses the source code and converts it into AST form
  • Babel transforms the source AST into a lower version AST
  • Babel will generate low version AST as low version source code, string form
  • Webpack parses the lower-version source code
  • Webpack packages multiple modules into the final product

The source code needs to go through string => AST => AST => String => AST => String, repeatedly jumping between the string and AST.

Esbuild overwrites most translation tools to share similar AST structures at multiple compilation stages, minimizing string-to-AST conversions and improving memory efficiency.

conclusion

In terms of compile performance alone, Esbuild beats all the packaging frameworks in the world by a hundredfold:

Time consuming Performance difference speed Product size
Esbuild 0.11 s 1x 1198.5 kloc/s 0.97 MB
Esbuild (1 thread) 0.40 s 4x 329.6 kloc/s 0.97 MB
webpack 4 19.14 s 174x 6.9 kloc/s 1.26 MB
parcel 1 22.41 s 204x 5.9 kloc/s 1.56 MB
webpack 5 25.61 s 233x 5.1 kloc/s 1.26 MB
parcel 2 31.39 s 285x 4.2 kloc/s 0.97 MB

However, this comes at a cost. Apart from the natural advantages of the language level, it directly gives up support for resources such as less, stylus, Sass, vue, Angular, MF, HMR, TS type checking, and other functions in the functional level. As the author says:

This will involve saying “no” to requests for adding major features to esbuild itself. I don’t think esbuild should become an all-in-one solution for all frontend needs!

In my opinion, Esbuild cannot replace Webpack at present and in the future. It is not suitable for direct use in production environment, but more suitable as a low-level module packaging tool. It needs to be re-packaged on its basis to expand a set of tool chain with both performance and complete engineering capability. Examples include Snowpack, Vite, SvelteKit, Remix Run, etc.

Overall, Esbuild provides a new design approach that is worth learning about, but not ready for direct production use for most business scenarios.