If you missed the previous chapters, you can find them here:
- An overview of the engine, the runtime, and the call stack
- Inside Google’s V8 engine + 5 tips on how to write optimized code
- Memory management + how to handle 4 common memory leaks
- The event loop and the rise of Async programming + 5 ways to better coding with async/await
- Deep dive into WebSockets and HTTP/2 with SSE + how to pick the right path
- The building blocks of Web Workers + 5 cases when you should use them
- Service Workers, their life-cycle, and use cases
- The mechanics of Web Push Notifications
- Tracking changes in the DOM using MutationObserver
- The rendering engine and tips to optimize its performance
- Inside the Networking Layer + How to Optimize Its Performance and Security
- Under the hood of CSS and JS animations + how to optimize their performance
How programming languages work
The parser will produce the following AST.
What’s more, web apps are getting more complex by the minute as more business logic is going to the client side to introduce a more native-like user experience. You can easily understand how much this is affecting your app/website. All you need to do is open the browser dev tools and let it measure the amount of time spent on parsing, compiling, and everything else that’s happening in the browser until the page is fully loaded.
Unfortunately, there are no dev tools on mobile browsers. No worries though. This doesn’t mean that there’s nothing you can do about it. This is why tools like DeviceTiming exist. It can help you measure parsing & execution times for scripts in a controlled environment. It works by wrapping local scripts with instrumentation code so that each time your pages are hit from different devices you can locally measure the parsing and execution times.
V8 for example does script streaming and code caching. Script streaming means that async and deferred scripts get parsed on a separate thread as soon as the download has begun. This indicates that the parsing is almost immediately done after the script is downloaded. It results in pages getting loaded about 10% faster.
The SpiderMonkey engine used by Firefox doesn’t cache everything. It can transition into a monitoring stage where it counts how many times a given script is being executed. Based on this count it determines which parts of the code are hot and need to be optimized.
Obviously, some take the decision not to do anything. Maciej Stachowiak, the lead developer of Safari, states that Safari doesn’t do any caching of the compiled bytecode. It’s something that they have considered but they haven’t implemented since the code generation is less than 2% of the total execution time.
Clearly, this is not a new concept. Even browsers like IE 9 support such type of optimization albeit in a rather rudimentary way compared to the way today’s parsers work.
Just like in the previous example, the code is fed into the parser which does syntactic analysis and outputs an AST. So we have something along the lines of:
Function declaration of foo which accepts one argument (x). It has one return statement. The function returns the result of the + operation over x and 10.
Function declaration of bar which accepts two arguments (x and y). It has one return statement. The function returns the result of the + operation over x and y.
Make a function call to bar with two arguments 40 and 2.
Make a function call to console.log with one argument the result of the previous function call.
So what just happened? The parser saw a declaration of the foo function, a declaration of the bar function, a call of the bar function and a call of the console.log function. But wait a minute… there’s some extra work done by the parser that’s completely irrelevant. That’s the parsing of the foo function. Why is it irrelevant? Because the function foo is never called (or at least not at that point in time). This is a simple example and might look like something unusual but in many real-world apps, many of the declared functions are never called.
Here instead of parsing the foo function, we can note that it’s declared without specifying what it does. The actual parsing takes place when necessary, just before the function is executed. And yes, the lazy parsing still needs to find the whole body of the function and make a declaration for it, but that’s it. It doesn’t need the syntax tree because it’s not going to be processed yet. Plus, it doesn’t allocate memory from the heap which usually takes up a fair amount of system resources. In short, skipping these steps introduces a big performance improvement.
So in the previous example, the parser would actually do something like the following.
Note that the foo function declaration is acknowledged but that’s it. Nothing more has been done to go into the body of the function itself. In this case, the function body was just a single return statement. However, as in most real-world applications, it can be much bigger, containing multiple return statements, conditionals, loops, variable declarations and even nested function declarations. And this all would be a complete waste of time and system resources since the function will never be called.
It’s a fairly simple concept but in reality, its implementation is far from being simple. Here we showed one example which is definitely not the only case. The entire method applies to functions, loops, conditionals, objects, etc. Basically, everything that needs to be parsed.
So why don’t parsers always parse lazily? If something is parsed lazily, it has to be executed immediately, and this will actually make it slower. It’s going to make a single lazy parse and another eager parse right after the first one. This will result in a 50% slowdown compared to just parsing it eagerly.
Now that we have a basic understanding of what’s happening backstage, it’s time to think about what we can do to give the parser a hand. We can write our code in such a way so that the functions are parsed at the right time. There’s one pattern which is recognized by most parsers: wrapping a function in parenthesis. This is almost always a positive signal for the parser that the function is going to be executed immediately. If the parser sees an opening parenthesis and immediately after that a function declaration, it will eagerly parse the function. We can help the parser by explicitly declaring a function as such that is going to be executed immediately.
Let’s say we have a function named foo.
Since there’s no obvious sign that the function is going to be executed immediately the browser is going to do a lazy parse. However, we’re sure that this is not correct so we can do two things.
First, we store the function in a variable:
Note that we left the name of the function between the function keyword and the opening parenthesis before the function arguments. This is not necessary but is recommended since in the case of a thrown exception the stacktrace will contain the actual name of the function instead of just saying <anonymous>.
The parser is still going to do a lazy parse. This can be prevented by adding one small detail: wrapping the function in parenthesis.
At this point, when the parser sees the opening parenthesis before the function keyword it’s going to do immediately an eager parsing.
So we’re coding as usual and there’s a piece of code that looks like this:
Everything seems fine, working as expected and it’s fast because there’s an opening parenthesis before the function declaration. Great. Of course, before going into production, we need to minify our code to save bytes. The following code is the output of the minifier:
It seems ok. The code works as before. There’s something missing though. The minifier has removed the parenthesis wrapping the function and instead has placed a single exclamation mark before the function. This means that the parser is going to skip this and will do a lazy parse. On top, to be able to execute the function it will do an eager parse right after the lazy one . This all makes our code run slower. Luckily, we have tools like Optimize.js that do the hard work for us. Passing the minified code through Optimize.js is going to produce the following output:
That’s more like it. Now we have the best of both worlds: the code is minified and the parser is properly identifying which functions need to be parsed eagerly and which lazily.
But why can’t we do all this work on the server side? After all, it’s much better to do it once and serve the results to the client rather than forcing each client do the job every time. Well, there’s an ongoing discussion whether engines should offer a way to execute precompiled scripts so this time isn’t wasted in the browser. In essence, the idea is to have a server-side tool that can generate bytecode which we’d only need to transfer over the wire and execute it on the client-side. Then we’d see some major differences in start-up time. It might sound tempting but it’s not that simple. This might have the opposite effect since it would be larger and most probably would need to sign the code and process it for security reasons. The V8 team , for example, is working on internally avoiding reparsing so that precompiling might not actually be that beneficial.
A few tips that you can follow to serve your app to users as fast as possible
- Check your dependencies. Get rid of everything that’s not needed.
- Split your code into smaller chunks instead of loading one big blob.
- Use dev tools and DeviceTiming to find out where the bottleneck is.
- Use tools like Optimize.js to help the parser to decide when to parse eagerly and when lazily.
SessionStack is a tool that recreates visually everything that happened to the end users at the time they experienced an issue while interacting with a web app. The tool doesn’t reproduce the session as an actual video but rather simulates all the events in a sandboxed environment in the browser. This brings some implications, for example in scenarios where the codebase of the currently loaded page becomes big and complex.
The above techniques are something that we recently started to incorporate in SessionStack’s development process. Such optimizations allow us to load SessionStack faster. The faster SessionStack can free the browser resources the more seamless, natural user experience the tool will offer when loading and watching user sessions.
There is a free plan if you’d like to give SessionStack a try.