JavaScript Performance - Part 1

JavaScript Performance - Part 1

The rising cost of JavaScript, Parsing, and AST

Β·

5 min read

Hi folks, this article is mainly my thoughts and notes on JavaScript Performance.

Most of the stuff I learned here from Steve Kinney and his JS Performance workshop on Frontend Masters.

I just distilled and synthesized my notes here.

The problem

We literally CAN'T buy Faster servers to improve the performance of client-side applications.

As the application grows bigger and bigger, we tend to ship more JavaScript code to the browser.

❓ A lot of time and energy is spent on compressing assets, removing requests, and reducing network latency, BUT what about when the application is running?

πŸ‘‰ Most of the time our application is running, it's parsing JavaScript.

πŸ‘‰ Sometimes, parsing and compiling are the real problem.

So, the question is HOW can we make this parsing time shorter?

In order to do that, we need to understand

HOW does JavaScript work?

πŸ‘‰ JavaScript is an interpreted language

πŸ‘‰ Most browsers use something called just-in-time (JIT) compilation.

πŸ‘‰ The JavaScript code is compiled moments before executed on your USER'S machine.

Things to understand about JIT Compilation

  • It means that there is a compilation step.

  • It happens moments before the execution.

  • It happens on our client's machine.

  • They are paying the cost and doing the hard work FOR us.

Here's your code's journey through browser V8 engine

  1. JavaScript lives in the Cloud, we send it to the browser, then...

  1. We have to Parse it

  1. We turn that into so-called an Abstract Syntax Tree (AST) - an actual Data Structure that represents what this code actually means.

    1. That then goes to the baseline Interpreter

    2. Then that gets turned into Byte Code which is understood by the machine

      This is an Intermediate Representation(IR) which is further optimized by the JIT compiler.f

    3. But, there is more. It's an Optimizing Compiler. That then looks at the code, and try to make it EVEN FASTER.

      Any code that can be optimized goes into the Optimizing Compiler to get optimized.

      That splits out highly optimized machine code (x86 ARM)

      Sometimes, the super-optimized code isn't gonna work out.

      We'll switch back to using the normal Byte code.

All images from Steve Kinney


PARSING

Parsing can be SLOW. As slow as 1MB/s on Mobile. 🀯

πŸ‘‰ One way to Reduce the Parsing time is to have LESS code to parse.

πŸ‘‰ Another way is to do as much parsing as you need and as little as you can get away with.

Parsing happens in 2 phases

  • Eager (Full parse) - This is what you think of when think about parsing.

  • Lazy (Pre-parse) - Do the bare minimum NOW. We'll parse it later.

Generally speaking, Lazy parsing is a GOOD thing. It follows the golden rule of performance. Doing less work is faster than doing work!

The basic rules

  • Scan through the top-level scope. Parse all the code you see that's actually doing something.

  • SKIP things like function declarations and classes for NOW. We'll parse them LATER when we NEED them

BUT this could potentially bite us.

// These will be eager-parse
let a = 2;
let b = 3;

// Oh! There's a function declaration here
// but we'll parse the body LATER when we NEED it.
function add(a,b) {
    return a + b;
};

add(a, b); // WOO! Go back and parse add()!

In this example, we come across a function declaration. We skipped that.

Afterward, we need the function, by calling add(a, b)

Do you see the problem here?

πŸ‘‰ We do lazy parse, then immediately, when we need the function, we go back and eager parse it.

πŸ‘‰ Not doing stuff is Faster than doing stuff, BUT, here we're doing stuff TWICE.

Doing stuff twice is obviously slower than doing just once.

In the above example, we can do this to avoid parsing twice.

let a = 2;
let b = 3;

(function add(a,b) {
    return a + b;
});

add(a, b);

By wrapping the function into parentheses.

The add function will get eager parsed.

Caveat

Because in the end line, we need to make use of the add function right away.

That's why we need eager parsed in this case.

It's definitely helpful to know HOW parsing works. BUT...

πŸ‘‰ It DOESN'T mean you should wrap every function declaration or class in your code into parentheses. BECAUSE

  • Not every function or class needs to be used right away. Why do they need to be eager-parsed in the first place?

  • Don't do micro-optimization when not necessary.

  • There's an algorithm trying to help you there. When you wrap every function in the parentheses you're basically OPTING OUT of all Lazy parsing. Which kind of defeats the purpose of it. It works well most of the time.

Try to avoid nested functions

Don't do this

function sumOfSquares(x, y) {
    // This function will repeatedly parsed. ❌
    function square(n) {
        return n * n;
    };

    return square(x) + square(y);
}

Better

function square(n) {
    return n * n;
};

function sumOfSquares(x, y) {
    return square(x) + square(y);
}

By moving the square function out. It can get parsed in a separate take on it.

It doesn't need to be parsed over and over again.

So we get better performance.

Our code is now parsed. What's next?

It's turned into an...


Abstract Syntax Tree (AST)

Essentially, we've gone from a BIG long string of text to an actual data structure representing our code.

With our AST, we now have everything we need to make byte code!

Then, the baseline Interpreter takes the AST and starts to execute our code as we wrote it.

Β