import statements and
new Worker() calls), inlining
process.env variables and other Node globals, and performing scope hoisting.
In addition, Parcel automatically transpiles your source code for your configured
browserslist targets, along with non-standard syntax like JSX and TypeScript, and development features like React Fast Refresh.
In addition, the scope hoisting linker now operates only on strings rather than ASTs, which also massively improves performance because we avoid serializing and deserializing these large objects. This also allows code generation to be parallelized across all files rather than performed on the whole bundle at once.
Overall, this improves build performance by up to 10x!
On the ESBuild benchmark, Parcel is now ~10x faster without Terser, and ~3x faster when minification is enabled.
¶ Transpilation performance
In addition, SWC replaces Babel by default for transpilation when a
browserslist is set, as well as for compiling JSX and TypeScript, and React Fast Refresh. SWC is 20x faster than Babel, so this change should also improve overall performance significantly.
While SWC is the default, rest assured that Babel is still supported as well. If you have a custom Babel configuration in your project, it will still be used automatically. This means that custom Babel plugins (e.g. CSS-in-JS transforms, Babel macros, etc.) continue to work out of the box. Scope hoisting, dependency collection, and everything else that was built into Parcel before will now occur in Rust, but this should be completely transparent.
This does open up the possibility to improve the performance of your build even further, however. You can now remove
@babel/preset-typescript from your
.babelrc and they will be automatically handled by SWC instead. This can significantly improve your build performance. If you have additional custom Babel plugins, you can leave them in your Babel config. If not, you can delete your Babel config entirely. We will likely add a warning in the future to assist with this migration.
¶ Scope hoisting improvements
When hoisting multiple modules together into the same scope rather than wrapping each module in a separate function, it becomes difficult to ensure that these modules always execute in the correct order when they are referenced across bundles.
In addition, sometimes small modules are duplicated between multiple bundles in order to avoid producing many very tiny output files and increasing the number of HTTP requests needed to load a page. Previously, this could result in a module executing more than once, which could break many things, including side effects (e.g. mutating the DOM) and singleton patterns.
In order to fix these problems, it was necessary to rethink how our scope hoisting compiler worked from the ground up. The result is a hybrid between scope hoisting where we can, but falling back wrapping modules in a CJS-style function with a global module registry where needed. This ensures that modules that are referenced between bundles execute in the correct order, and that duplicated modules only execute once.
If you’re curious to read more about how our scope hoisting implementation works, check out the design document, which goes into detail on all of these topics.
¶ Tree shaking dynamic import
Another feature related to our scope hoisting implementation is support for tree shaking dynamic
import(). Parcel can detect which properties of a dynamic import are accessed, and exclude the exports from the resolved module that aren't used. This works with promise chaining, async/await, destructuring, and static object property accesses. If anything is accessed non-statically, e.g. a computed property, then all of the exports will be included.
¶ Tree shaking CSS modules
¶ Lazy development builds
In development, it can be frustrating to wait for your entire app to build before the dev server starts up. This is especially true when working on large apps with many pages. If you’re only working on one feature, why do you need to wait for all of the others to build unless you actually navigate to them?
Parcel now supports a
--lazy CLI flag when using the dev server. When enabled, Parcel only builds files that are requested on demand, which can significantly reduce development build times. The server starts instantly, and when you navigate to a page for the first time, Parcel builds only the files necessary for that page. When you navigate to another page, that page will be built on demand. If you navigate back to a page that was previously built, it loads instantly.
This also works with dynamic
The tradeoff, of course, is that page loads and dynamic imports can be slightly slower the first time they are loaded. But with Parcel's disk caching, that should only ever happen once. Even when you restart Parcel, there should be no need to rebuild files that didn't change.
¶ Cache reliability
Parcel tracks all of these inputs in a graph. Our file watcher watches for changes on disk, and invalidates any requests that are connected to the files that changed. This also occurs when you restart Parcel. Our watcher integrates with operating system level APIs to quickly determine what files changed since you last ran Parcel, which means that restarting Parcel is almost as fast as using watch mode!
¶ Dev dependency HMR
A side benefit of all of this work on cache invalidation is that watch mode also benefits. One cool feature that we’ve implemented is HMR for Parcel plugins and other build dependencies. When you make a change to a plugin's source code, we reload the plugin in place and rebuild any files that it had previously compiled. This makes it super fast to work on a plugin and see your changes instantly – no need to restart Parcel.
It also works with Babel plugins, PostCSS plugins, and any other dev dependency involved in your build. You can even edit plugins in
node_modules and Parcel will recompile automatically. This is useful when you need to debug something in your build pipeline, or make use of tools like patch-package.
¶ Fewer dependencies
Parcel supports many different languages and tools out of the box, which makes it really easy to get started. But one downside of this is that installing Parcel includes many dependencies that you might not use. This not only uses up disk space and network bandwidth, but is also more dependencies for you to maintain and audit.
We wanted to reduce Parcel's dependencies while still keeping things really simple to use. To do this, we now only install essential plugins by default, and auto install additional plugins into your project on demand.
Parcel 2 has been in development for a couple years now, and we haven’t always done the best job of updating the community on where things stand on our roadmap to a stable release. So, this section is an update on our progress.
Beta 3 will most likely be the final beta before our first release candidate, which we're hoping will be out in about a month. For the first rc, we are working on the following items:
- Improved cache invalidation for packaging and optimizing
- Ensuring the cache is portable between machines or different file paths (e.g. in CI)
- Improved automatic differential bundling support (module/nomodule)
- API consistency
After rc, the public API will be frozen, and we’ll focus on bug fixes and documentation. This will hopefully take around a month. Once ready, we’ll release 2.0!
At this point, it is very likely that the plugin APIs for bundlers, runtimes, and validators will be marked as experimental in Parcel 2.0. This means these features will not follow semver and we will continue iterating on them after the initial stable Parcel 2 release. In addition, we have many other features and improvements planned for post-2.0, including further performance optimizations.