June 27th, 2024

Optimizing JavaScript for Fun and for Profit

Optimizing JavaScript code for performance involves benchmarking, avoiding unnecessary work, string comparisons, and diverse object shapes. JavaScript engines optimize based on object shapes, impacting array/object methods and indirection. Creating objects with the same shape improves optimization, cautioning against slower functional programming methods. Costs of indirection like proxy objects and function calls affect performance. Code examples and benchmarks demonstrate optimization variances.

Read original articleLink Icon
Optimizing JavaScript for Fun and for Profit

The article discusses optimizing JavaScript code for performance while balancing readability. It emphasizes the importance of benchmarking before optimization and provides tips such as avoiding unnecessary work, string comparisons, and different object shapes to enhance efficiency. It explains how JavaScript engines optimize code based on object shapes and highlights the impact of using array/object methods and indirection on performance. The author suggests creating objects with the same shape to improve optimization and warns against functional programming methods that can be slower than imperative approaches. Additionally, it addresses the costs of indirection, including proxy objects, object access methods, and function calls, on performance. The article includes code examples and benchmarks to illustrate the differences in optimization techniques.

Related

Exposition of Front End Build Systems

Exposition of Front End Build Systems

Frontend build systems are crucial in web development, involving transpilation, bundling, and minification steps. Tools like Babel and Webpack optimize code for performance and developer experience. Various bundlers like Webpack, Rollup, Parcel, esbuild, and Turbopack are compared for features and performance.

Understanding React Compiler

Understanding React Compiler

React's core architecture simplifies app development but can lead to performance issues. The React team introduced React Compiler to automate performance tuning by rewriting code using AST, memoization, and hook storage for optimization.

Optimizing the Roc parser/compiler with data-oriented design

Optimizing the Roc parser/compiler with data-oriented design

The blog post explores optimizing a parser/compiler with data-oriented design (DoD), comparing Array of Structs and Struct of Arrays for improved performance through memory efficiency and cache utilization. Restructuring data in the Roc compiler showcases enhanced efficiency and performance gains.

Understanding React Compiler

Understanding React Compiler

React's core architecture simplifies development but can lead to performance issues. The React team introduced the React Compiler to automate performance tuning by rewriting code. Transpilers like Babel convert JSX for efficiency. Compilers, transpilers, and optimizers analyze and produce equivalent code. React Compiler enhances functionality using Abstract Syntax Trees, memoization, and hook storage for optimized performance.

New Web Development: Or, why Copilots and chatbots are bad for modern web dev

New Web Development: Or, why Copilots and chatbots are bad for modern web dev

The analysis critiques Copilots, chatbots, and React for web development, citing slow sites, complex code, and high costs. It advocates for a shift to browser APIs, CSS, and HTML for better performance and lower expenses. Transition challenges include finding developers skilled in vanilla JavaScript. Organizations are urged to prioritize simplicity, efficiency, and core web technology training.

Link Icon 6 comments
By @gizmo - 5 months
> But [the strcmp] is there, and a string comparison will usually (but not always) require comparing each of the characters in the string with the ones in the other string

This is not true in anything except for toy interpreters, for a number of reasons.

One, internally strings are stored alongside the length. For 'TOP' and 'BOTTOM' the lengths don't match which means the strings can't be equal. That means inequality testing requires only a single integer comparison.

Two, strcmp is used for ordering strings, that's why it returns -1, 0, 1. That additional work isn't needed for testing equality. JS engines are not going to call strcmp unnecessarily.

Three, modern Javascript engines don't treat all strings the same. There are fast paths for ASCII strings, for interned strings, for concatenated strings, for strings that are slices of other strings, and more. From the perspective of the programmer a Javascript string is just a string. In reality a string in Javascript is nothing like C's char*.

I suspect -- but don't actually know -- that the string compare in the benchmark of example 1 is slower because the JS engine has to do a type check on the internal string representation. That's an extra branch the integer path doesn't need.

By @seabass - 5 months
Loved this write up. Of course the usual caveats with this level of optimization apply, and in general we should be aiming to optimize for human readability. But if nothing else I think it’s a good thing to get a better internal understanding of how the interpreter works.

With that in mind, it’d be interesting to see the relative costs of each deoptimization to the others. For example, is a monomorphic->polymorphic deopt an order of magnitude worse than an unnecessary string mutation? In nanoseconds, what’s the actual cost of each of these? Because there are certainly going to be some optimizations that matter much more than others.

By @emmanueloga_ - 5 months
Lots of good points here. The problem with JS optimization is that, as developers, we are basically flying blind.

Looking into how to get more debugging information out of a JS engine, I found this V8 blogpost that explains how to install a debugging version of V8 called "D8" [1].

I wish getting debug info was way easier though. Imagine being able to do something like this:

    function xyz() { "use debug"; }
--

1: https://v8.dev/blog/elements-kinds#debugging

By @hsshhshshjk - 5 months
For the functional vs imperative... Why make the example array.map.filter.reduce rather than just a single reduce? I find map.filter.reduce to be a gross anti pattern.