Transpiling private member fields with Babel

The current project I am working on deals with a lot of JavaScript. It is actually 100% TypeScript. JavaScript execution is made very fast in modern browsers, especially the V8 engine, thanks to technologies like hidden classes. JavaScript is a bolt-on language with humble roots to a modern scripting language thanks to all the ES proposals that eventually became standards. One such proposal is private fields. The product I am working on requires heavy computation on the front-end and every bit of optimization counts. The good thing about private fields is that they allow browsers to push the optimization boundaries as unlike the regular fields, private fields have to be declared before they can be used. This essentially means the optimizer can make decisions based on static code rather than deferring it to runtime and adjusting it on the fly as needed like how it’s done with the hidden classes.

While the private fields are very promising, as of this writing only Chrome has production support, Firefox support is almost there with the functionality currently hidden behind configuration (otherwise, it throws the error “Uncaught SyntaxError: private fields are not currently supported”). Safari has just started private fields support from 14.1. That means, for the time being the code needs to be transpiled to support older browsers or Firefox. The transpilation is possible with tools such as Babel.

I recently found esbuild that can transpile and bundle code and is much faster. However, I needed to integrate the code as a component into Docusaurus documentation. Docusaurus comes with its own build process and internally uses WebPack and Babel. So, for this specific use case I am stuck with Babel. However, the actual setup was TypeScript code compiled by tsc, transpiled and bundled by esbuild and finally the resulting bundle is further transpiled and bundled through Babel and WebPack.

Docusaurus has two modes of building, one for development (or should we say, documentation) while the other is for production. The development mode worked just fine but the production mode was resulting in runtime errors related to private field not being present and that’s where the problems stared. On careful observation of the stack trace, I identified the problem that is a combination of several things.

  1. The way esbuild (and Babel) transpile private fields to older targets is by using WeakMaps. Then at runtime, read/writes are checked against the WeakMaps with the object as the key and each private field has its own WeakMap.
  2. In order to provide access to the private variables, corresponding property getter/setters are defined.
  3. Occasionally, a Child class has to override these setter/getters and use the super keyword to delegate to the Parent class.
  4. Babel has an option called loose and if it’s specified as an option to the preset, then any plugin which supports that option is also provided the same value.
  5. Docusaurus uses loose=true and as a result all the class transformations are being done loosely, so to speak.
  6. One such transformation is the super which became a.prototype. Hence, super.xyz becomes t.prototype.xyz where t is the variable name given to this in that context.
  7. This is where the problem is. The esbuild code has logic while accessing the private field to check for presence in the WeakMap and ideally the look up should be this but in this case, it is t.prototype and hence it doesn’t find the field and thinks that the code is trying to access a private field that is not owned by the object.

After a bit of trail and error, I managed to solve this by specifying loose as false for the plugin “@babel/plugin-transform-classes” and everything started working as expected. Of course, by being strict with the transformations there is a lot of performance penalty on top of already losing performance in not using the private fields itself.

Obviously, correctness has higher priority to performance. So, for the time being, the documentation will support all the browsers with slightly lower performance and this situation will be reevaluated in the future when private fields become widespread.

This entry was posted in JavaScript and tagged , , . Bookmark the permalink.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.