Hello.
I'm Kang Jaeseok and I'm here to talk about 'Javascript Async for effortless UX'.
Sharing this content with all of you here,
and at a grand stage like the JS Conf Korea- it's a true honor.
There's even small commonalities like this.
What I'm going to talk about today is...
this is the general flow of my story.
First, we are going to talk about a problem.
Then, we are going to analyze the cause, and based on the cause, we are going to seek for some solutions.
First, I'd like to handle this type of problem.
We encounter this problem from time to time when using web applications.
The scroll stops, or what I typed doesn't show on the screen right away
or the animation isn't smooth.
As you can see, these are the main examples that can distract users.
There are various causes to these problems.
First, the animation itself could've been wrong.
There could be a problem in the environment.
But in this session, we are going to analyze the cause focused on these factors.
One of the features of Javascript language: the run-to-completion,
Javascript engine, and the Call stack event loop are the three factors.
And based on the analyzed causes, I'm going to suggest some solutions.
One thing I want you to keep in mind before we start,
is that the things I'm going to explain aren't the exact solutions
but are part of the progress in finding a better solution to a problem.
Before we start, what is a user experience?
I'm not a UX designer or an expert in that field.
So I'm being careful here but,
when you think about what a user experience is,
after all, it's about the user's feeling towards the product, system and service.
Then what are the common features of these problems?
The interaction that the user intended, like page loading, typing and animation, didn't work in time.
If the interaction doesn't work in time
Like I said, in the user experience's point of view, it's a negative factor.
Then what caused these problems?
If the intended action doesn't act in time,
we can suspect that something is blocking the act and making it slow.
To unravel this suspicion, let's take a look at how the Javascript code works internally.
First, I want to mention the traits of Run-to-completion.
Run-to-completion is, when a process of one message starts,
no other process can get in the way until this message process is finished.
We'll look at some example code.
What would happen if we run this code?
Set running to true first,
and the setTimeout function can run.
Then, start the while loop
and check if the running has changed; but it would still be true.
Then the running message would be on the console.
After about 500 ms, running still would be true.
Then we would never be out of the while loop.
You could also encounter this familiar icon.
As you all know, this is an icon shown when the Chrome tab process is dead.
As a developer, we should understand the run-to-completion process well and work hard not to meet this icon.
So, I've shown you the process with example code to explain the run-to-completion process.
In the Javascript engine, there's a place that acts as a curser to mark where the code is being processed.
That is the call stack.
Every time there's a request in Javascript, the requests are kept in call stack in order and are processed.
For example, they show what function is called and acted,
and they manage the function that should be called next.
This is code that calls up the hello function, that has code which writes 'Hello',
and also defines the helloJsConf function that has code that writes 'JsConfKorea',
and ultimately calls up the helloJsConf function.
While this code is processed, let's see
what happens in the Call stack.
First, the main code block is stacked
and by calling the helloJsConf function, it joins the stack.
Then it calls the hello function
and it would request to write the hello message on the console.
By writing the hello message, it's eliminated from the stack.
JsConfKorea, which was next to the Hello function call, requests to write 'Js Conf Korea' message and gets eliminated from the stack.
So helloJsConf function has done its work, so it's eliminated form the stack.
After all the work is done, the main block is eliminated.
As you can see, Javascript acts with the Call stack structure and run-to-completion method.
When these methods are acted out, what kind of situations would cause a problem?
Let's give an example situation
If you were processing each request at the call stack, and you come up with a task with some delays, what happens?
It operates in the order of requests, like in the exercise before
And it encounters a request that takes a lot of processing time, like the function 'someExpensive',
then there will be delays in things like displaying 'hello' or 'JSConfKorea', which was performed instantly before.
Then we have a question here.
Javascript is supposed to process multiple tasks at a single call stack structure,
but when we think of using web services
we click on something,
we scroll, and we type, and the data is added and displayed on screen,
and it doesn't look like the tasks are being waited upon and processed in order.
Then, how are they managing these complex sync issues?
The following could be the answers to this question.
Web APIs that help the solution of synchronistic issues,
DOM events or XMLHttpRequest; the api's used to call Ajax.
There are also setTimeout, which manages the timer, Promise, or RequestAnimationFrame.
It's hard to not use these in contemporary web development.
And the event loop, which I will discuss further in the next chapter.
These two enables asynchronous programming with Javascript.
Then we will examine the event loop in depth.
MDN describes the event loop with codes, like shown.
To interpret: assuming that waitForMessage function operates synchronously
the code runs the infinite loop waiting for messages, and if there are any, the code would process the next message.
The event loop is not a component of the Javascript engine.
Which means, supporting asynchronicity
would not be the Javascript engine, but the environment that the engine is driven on, like the browser or node.js.
This is where the event loop is grounded.
In such an environment, the loop controls which task to shove into the call stack.
This is a simple explanation of how the event loop works.
If there are tasks, execute the oldest one,
And there aren't any tasks, wait for one, and if there's any, go back to number 1 and repeat.
Now I will discuss the event loop further with the aforementioned code, using web APIs that manages asynchronicity.
Look at the code here: setTimeout is, as you know,
a function that creates timer events, wait for a time, and executes them.
Like you see in the example code, if there is no factor it will be the default value, 0.
The 0 in the timer gives the feeling it should be executed right away, but that's not true.
The reason for that, we will find out the steps of how the code works.
The Promise is an API dealing with future situations in which asynchronous operations have succeeded or failed at processing.
This code restores the Promise's transitioning to empty values with the resolve method,
and hands the callback over, when the transition is completed with the then method.
Let's take a look.
You see, first the setTimeout is called.
And the callback goes to the task waiting line.
Promise is called over,
and the part that was handed over to the then method through callback
It should be in the task waiting line
but as you can see, its in a slightly different waiting line.
In ES6, to deal with the synchronicity, APIs like Promise are added
but these deal with tasks slightly differently from regular tasks, which are called microtasks.
The difference between tasks and microtasks is this.
Tasks are the things that need to be executed in order, in the browser or other places.
Their sources can be regular script execution, setTimeout, or callbacks from UI events.
Microtasks are asynchronous tasks that come right after the task that's being executed now.
Which means that microtasks have higher priorities than regular tasks.
Like I mentioned, its sources could be the Promise, Observer API, and the process.nextTick of Node.js.
And so coming back to my earlier definition of event loops, we'll need a little modification.
If you look at it, before waiting for the task,
it needs to check if there are any microtasks
and if the microtasks are empty, then it will run the tasks.
Then, when we come back to the example code, now we can see what the event loop does.
The callback is handed over to the Promise,
it is pushed into the call stack through the event loop as a microtask,
and it is executed.
And the task that displays "hello" would be executed.
Then, based on the understanding of event loops,
can we solve all the problems if we use web api's dealing with asynchronisity?
Unfortunately, no.
Still, because of the tasks in front of that, there is still possibility that the next tasks may be blocked.
Shall we take a look?
Like the example before,
call setTimeout,
put the callback in the task queue,
then call the Promise,
The callbacks...
One second,
The callbacks to execute the expensive tasks of then go into the microtask queue.
As I have mentioned, because microtasks have higher priority,
the event loop has pushed higher-cost operations into the call stacks first.
Therefore, the task of hitting 'Hello'
will be blocked by the event loop.
Obviously, if the task is completed, then it could be executed.
So, though it took a while to get here,
to summarize, the causes and process of inconvenient UIs and UXs are as follows.
Because tasks are always executed sequentially by the event loop,
other tasks cannot be performed until a given task at hand is finished.
And since microtask queue has higher priority than other task queues,
tasks like UI events cannot be executed until all micro-tasks in queue are cleared.
Thus, tasks which take long time to execute,
for example, if tasks and microtasks involving multiplication calculation of CPU bounds are being executed,
then events directly related to UI, such as click, text, rendering etc, can be blocked.
This means that this may be 'an element harmful for user experience'.
Then, how can this blocking be resolved?
I will illustrate through the demo I've prepared.
This material is already public, so please feel free to look it up if you are interested.
Prior to the demo,
let's take a look at how the demo works, through the code.
It's simple code.
It's code that whenever a text input event occurs,
it creates HTML of a rectangular element of random color, proportional to the number of input characters,
and displays it on the screen.
There are two main points here.
There are many iterations,
and that the cost of DOM renewals are high.
I think this scenario of CPU bound high-cost operations would be sufficient to recreate the same blocking.
Let's take a quick look at the demo.
It's a simple UI.
There's a text input box in the middle, and the animation surrounds the box.
When I type a message, DOM whose number corresponds to the length of the message I typed, would be renewed on the screen.
And I'm in the perspective of user interaction this time.
I'd like the audience to focus on whether my text inputs are being well-reflected,
and if the animation is functioning properly.
I will try typing a message here.
You can see that the text freezes at about J, and the animation freezes as well.
And the rendering is reflected after a few whole seconds.
Coming back,
this is the result I got from profiling the demo using Chrome developer tool.
It looks a bit complex, but there are only two main points to focus on.
First, the top frame section.
I don't know if you can see here..
But, almost three seconds were consumed for just 1 frame.
Next, regarding the user interaction in this interaction section below,
you can see the analyzed results.
The red underlines within the orange blocks refer to the time consumed waiting for the main thread of user interaction.
So, the very long red underlines mean that
considerable time has been spent for each interaction.
So, now I should introduce resolutions for minimizing these problems.
I have two main solutions.
First,
the method of delegating the task to another thread,
since the block happens because of the structure of a single call stack and event loop.
Meaning that we could try multi-threading, if it's possible on Javascript as well.
Or another solution, if the task causing the block is too heavy,
then we can split the task into smaller tasks.
Multithreading on javascript can be done through web workers.
Web workers enable script operation in the background thread, which is separate from the main thread.
Worker thread is created off the main thread,
so that message-based communication with the worker thread is possible.
Through postMessage, execution of long running tasks can be requested to the worker thread.
The worker thread will execute this,
and there will be no blocks in the main thread due to this execution.
When the task is complete, results will be delivered through postMessage again.
And the main thread can then do something else with it.
Similarly, looking at this from the code,
the explanation is pretty much the same.
Worker object will be created, and through the postMessage method of the worker object,
the long running task can be requested to the worker thread when necessary,
and when the worker thread has completed it, the code will handle the results.
Then, we will take a look at a demo which utilizes this method.
I'll type the same message.
Okay, doesn't it seem better than before?
But, it still seems a bit slow.
Then, let's take a look at the demo profiling results which have been calculated with the explained methods.
Let's see.
You can see that the duration of frame and the duration of interaction
have very much decreased.
However, as seen in the demo, this method too fails to provide lag-free usage.
Most of the display devices these days are 60fps, I believe?
Considering the display at the standard of 1 frame per 16ms,
if you look up here, it is taking around 200ms, 600ms and if longer it takes up to around 900 ms.
My suggested reason, the operation that is CPU bound has been delegated to the worker thread
but operations like DOM renewals are still happening in the main thread.
The limitations of web workers are that the main thread and the worker thread can only communicate via a system of messages.
Meaning, the worker cannot access the DOM or the context of the main thread directly.
Next, there should be a way to run tasks asynchronously by splitting the heavy tasks into smaller ones.
It should look something like this when visualized.
If tasks in the back are getting blocked due to long-running tasks in the front,
this is a way of scheduling the tasks by splitting the large task into pieces, and placing them between other tasks.
Did you enjoy the previous session on generators?
I used the generator as well
to create an environment of scheduling,
where you can easily manage your schedule.
This is also open so feel free to take a look if you'd like.
I don't know if you can see the code.
Unlike executing the work once input event is triggered from the existing code,
here it is calling chunkGenerator on an interface called runChunks, and is passing it on.
chunkGenerators slice the existing iteration into appropriate chunk units
and have a pattern of yielding them at the corresponding units.
runChunks take the works yielded
and take the role of loading them into the task queue via asynchronous APIs, like the aforementioned setTimeout.
Then let's take a look at
the demo with this method applied.
I will write the same message.
Yes, in comparison,
you can see that text input is blocked a lot less
but still..
rendering is getting delayed and happening all at once.
At least text input is not blocked,
at least we've took care of that.
Likewise, let's look at the results from profiling.
Here too, we can see that there has been a big improvement in the numerical aspect as well.
Since we are already here, let's try to optimize it even more.
It seems that the cost of updating DOM is quite high.
We will approach it in the direction of minimizing the DOM update.
The demo updates DOM depending on the text input,
so I think it should be fine to only renew it at the last moment like before,
and not renew it for every typed input, like in the last scenario.
When the task is in a big chunk, the next task gets blocked so it cannot decide whether to update and continue the execution
but since it has been sliced into smaller chunks
applying this method would be possible.
For example, you can check if it is in the middle of executing the task,
and it if is, then you can cancel the corresponding task and execute a new task.
Shall we take a look?
As you can see, DOM is updated only once, when the final work is processed
and the animation does not seem to stop.
Text input is not blocked either.
The profiling graph seems very tidy as well.
Maximum duration of frame lasted a little longer at the moment of updating DOM
and the rest of frames are quite good.
Duration of interaction is really short too
Oh, time flies.
This is it, let me recap.
When long-running tasks or microtasks block events like rendering, click, and text input,
it is possible that a UI can harm user experience.
This is
due to the structure of the JavaScript engine, event loop and etc
and one must understand it and handle it properly.
And in order to handle this, one must delegate long running tasks to background thread like a Web Worker.
Or, one can split the long-running task so that other important UI events are not blocked.
That is the appropriate method;
This is the summary.
Actually, the demo I showed you and the assumed situations are rather extreme.
It's hard for that to happen in common app codes as well,
and it's a situation that you must try to avoid.
We say the best way to optimize is to just avoid it
and it is true that we should avoid expensive operation or excessive DOM updates.
Nevertheless, the reason for me to come up here and share this topic,
is that even if codes run smoothly, them being written with or without proper knowledge on their operation
has a significant impact in the long run.
I hope my intentions have been conveyed well. I will end it here.
Thank you.