Breaking up Long Tasks
Introduction to Breaking Up Long Tasks
Nishu Goel introduces the concept of breaking up long tasks in browsers, explaining the significance of understanding and managing tasks to improve web performance.
Demonstration of Long Tasks in Action
Nishu demonstrates how to identify long tasks using the browser's performance profiler, discussing the impact of such tasks on user experience and highlighting the importance of CPU throttling in testing.
Understanding Long Tasks
The concept of long tasks is explained, including how they are identified, their impact on the main thread, and why they represent a critical challenge for web developers to address.
Introduction to Core Web Vital: Interaction to Next Paint (INP)
Nishu introduces a new Core Web Vital metric called Interaction to Next Paint (INP), which aims to quantify a page's ability to avoid long tasks and improve interaction responsiveness.
How to Deal with Long Tasks
Strategies for breaking down long tasks into smaller, manageable chunks are discussed. The concept of yielding to the browser to prioritize tasks and improve responsiveness is introduced.
Advanced Techniques and New APIs
Advanced techniques for managing tasks, including the scheduler.yield and postTask APIs, are presented, with a focus on how they allow developers to control task prioritization more effectively.
Utilizing isInputPending API
Examination of the isInputPending API developed by Facebook and Chrome, which provides signals to developers about when to yield execution to the browser for pending user inputs, enhancing responsiveness.
Framework Support and Concurrent Rendering in React
The support of frameworks like React with concurrent rendering is discussed, showcasing how frameworks can help in managing task prioritization and improving web performance.
Conclusion and Additional Resources
Nishu wraps up the presentation with a summary of key points discussed and leaves the audience with additional resources to explore on the topics of optimizing tasks on the browser.
Okay.
So I'm going to be talking about breaking up long tasks.
What are long tasks?
How do we break them in the browser and so on?
It feels incomplete if I don't introduce myself.
So I'm still going to do it.
My name is Nishu Goel.
I work as a software engineer and a Google developer expert for web.
So before we actually start, I'm going to ask you how many of you are aware with what long tasks are, or have heard the term before?
Okay.
Lucky people.
Good that you don't have to know about it.
I would have loved that.
Now that not many people in the audience are aware of what tasks or long tasks are, let's go from the very beginning and see what a task really is.
I'm not going to give you the definition of what a task is, but instead actually show you in the browser.
Let's do that.
Is it visible or do you think I should zoom it a bit?
Let's see.
I'm gonna zoom the UI a bit.
What I'm trying to show you now is, I'll record a performance profile.
You might have done this before, where, you start the performance profiler, which, records some activity that you do on the UI, and then it tries to tell you something, which can get intimidating at times, but let's try to understand what's going on.
I have a Mac M2 and it's not a good idea to profile on that because, it's the gap is so much from what really the situation is with our users, customers, and so on.
So there's a good thing in, over here, if you see, there's the CPU throttling then you, that you can.
Switch down.
So I usually keep it at 4x slowdown.
And that's when I record my profiling, because that's when I get a bit closer to how the users are experiencing the website.
So I'm going to record a very short profile.
Let's start the profile over here and just, for example, put something in the number attribute and stop it.
Intimidating already, right?
I don't understand what all of that over there is.
So let's see if we can, first of all, get rid of the extra stuff from here.
Yeah, now it looks better, right?
It's all the main tab over here.
Do you see some red things over here?
Something like this.
So I already understand that this is some task, it says so, but it also isn't red.
And there's this little red triangle on the top right, which says, by the way Long task took 446 milliseconds.
Any idea what it's trying to say?
Probably something that shouldn't have happened, right?
Because that's what kind of red denotes.
A task is basically any discrete activity that you do in the browser that happens on the main thread.
It could be any interaction that you do, it could be any event callbacks, it could be just parsing HTML, and so on.
Now, when does a task become a long task?
That's an important question, right?
That's an important question to understand.
How do you control that my task, my activity that I'm doing with the browser is actually becoming a long task?
I don't know.
To understand that, we need to see what a task really does.
In this example, you can see it's trying to, let me see if I can expand this a bit, or just go into this bottom up tab over here.
So I see these are some of the activities that this particular task is doing.
There's some event timing.
It seems there was this function call that happened when I, actually entered the numbers in the number attribute.
And that led to a lot of things.
And there was some timer fired and so on.
Again, it's difficult to understand what really is going on.
What do I understand from timer fired?
So now we can go back.
We have a picture from what it looks from the performance profiling.
Now I'm actually going to go back to the slides and we know now what, now, yeah, we know now what a task really is.
It's an activity, which can be an interaction from me, from the user, which is really about running it on the main thread.
Now, you might've heard main thread from the beginning, if you're interacting, the, if you are attending the engineering or the React sessions.
So we have an idea of what a main thread is, but how do we control it?
Do we have a control over it?
Really?
We now know that there's something called long task, which could look like this.
We saw that there's some redness.
There's a little red triangle that leads, that kind of shows you that, okay, it's going beyond some time.
We don't know what that time is, but there's actually a threshold for that, which is 50 milliseconds.
And that means that any task that goes beyond that threshold of 50 milliseconds becomes a long task.
Now, you might ask me, what if there's something big that I want to do in the browser?
What if that's taking long?
How do I ensure that it goes in that, time of 50 milliseconds?
Now you see this particular task is evaluating some script.
It's compiling some code and already becomes around seven, 77 milliseconds for the main task.
But then, there's also some blocking time and this is what's called input delay.
And this is what leads to interaction delay for the user.
For example, this task was already going on and then comes in the user and clicks on a button.
Now, the user expects some feedback, right?
They expect that as soon as I click on the button, I should see something at least a spinner, if not a positive confirmation or something.
But for that, the thread is busy.
Remember, there's only main thread where most of the tasks are running and that's bloated right now with a lot of tasks.
Looks like there's some tasks like this, which is already keeping it busy.
Now what happens to that button click?
How does that get handled?
Now, the main thread lead needs to know that, okay, there's something that's coming in and I need to somehow maybe prioritize it because that needs to be, seems like more important because user is waiting for a feedback.
That's when, I was talking about, painting something on the UI first of all.
And, when the browser has to paint something in a new frame, because they want to avoid, give that visual feedback and avoid that interaction delay.
That's when this new Core Web Vital comes into picture.
It's called Interaction to Next Paint, or INP.
The idea is, it's a metric which basically, quantifies a page's ability to avoid long tasks, in simple terms, or avoid the browser from freezing completely.
If you've had bad, websites, slow websites, you might have seen some examples where it gets really extreme and the tab completely freezes.
Was it a long task?
Maybe.
Now we know how we can actually look in the profiling.
Was it stuttering, the website?
Was it a janky feeling when you clicked on some buttons or when you were typing some input?
You might have also observed that when you type on some inputs, especially in Safari, I've seen that happen a lot, there's a lag, right?
You type in and it comes after a bit.
We are probably setting some state there, which is, leading to that situation, but we'll come back to that later.
So INP is that Core Web Vital.
It's a pending metric right now.
It replaces FID, which is First Input Delay.
I'm actually curious to know how many of Core Web Vitals and what are the three Core Web Vitals right now.
Anyone?
Okay.
Interesting.
There's one Core Web Vital that exists already, which is called First Input Delay, and that used to measure the, time to that first paint.
However, there's a difference in that, in this metric, which is INP, it measures every input, paint and how much times, time it takes to render that.
So that's the main difference.
INP also gives you the worst interaction latency.
So it gives you the real scenario.
I used to be very happy when I used to profile or, see the scores of my websites, because at least FID used to be good.
And I'd be so happy that, Oh, maybe something I'm doing something right, but then comes INP and it gives you the real picture.
It's good to know the reality, but I'm sad that there's no metric now which looks good.
Yeah, that's what INP does.
Not gonna go into the details of how it works, what are the, details of INP and so on, but, yeah, this is the link to go for, and, yeah this blog tells you everything about INP.
Then, now we know that there's this threshold of 50 milliseconds.
And if we have a long task, which we might know in some cases that this particular activity that I'm doing might be, taking some time off the main thread and I would want to maybe split it into smaller tasks so that I ensure that at least one task is not taking that hundred millisecond or 200 milliseconds and so on.
At least that is making so many smaller tasks that each task is done in 50 milliseconds or lesser or around that.
But then you might ask me that the task is still going to take the same time.
Even if you break that down into smaller chunks, how does that help?
Now, this is where yielding comes into picture, which is where you tell the browser that there's smaller chunks of tasks and this particular task might be more important for the browser to respond with right now, because the user is waiting and you tell the browser to prioritize it.
But that seems like a new concept, right?
How do you tell the browser to prioritize something?
How are you interacting that much with the browser and communicating that much with the main thread that you just tell it, come on, prioritize it, this is more important.
So that's what yielding does.
Now this is a simple function that you can create yourself, which is basically returning a promise that only resolves with a call to set timeout.
And what this thing over here does is it sets that callback.
It starts that callback in a new task.
So you remember that long task from earlier?
That would already break into two smaller tasks with this.
And how would you use it with something that needs to update the UI?
So let's look at an example of the usage.
So let's see, there's something like this.
There's this one main function that saves the form.
It has to do some big tasks like validating a form, showing spinner.
Saving to database and so on, and then there's one which is updating the UI, right?
And that seems like something that user might wait for.
It could again be a spinner or some confirmation that okay, the process is running in the background and so on.
Some visual feedback, so at least you're communicating to your user.
Now for that update UI step, it's much later in the process, right?
And the main thread could already be blocked in the first three steps.
How do you communicate?
So at the update UI step, you want to tell the browser now that hey, I know that the main thread is busy, but can you maybe, prioritize the update UI, step and take that as the first thing in the main thread?
So rest of the things keep happening, but at least this one happens first, even if the main thread is blocked.
So that's where you could use that yield to main function.
And what this would do is exactly what we were talking about.
It would tell the main thread that, okay, update UI seems more important.
Let's take that as the first thing in, in that set of things that were happening on the main thread and then continue with the rest of the things.
But it's not that simple always, right?
What's the catch?
So the problem here before this is that when you do a setTimeout, It ensures that the yield domain method takes care of, taking update UI to the first, but everything else becomes or goes to the end of the queue.
That means any other action that happens in the meantime is taken care of first.
And everything else, those big tasks from here, go to the end of the queue.
So if the user is interacting more with the website, that takes, that becomes ahead in that queue, in that main thread.
And it can lead to inconsistencies, right?
Because you wanted that there's a little delay maybe for the rest of the things.
But not going to the end of the queue.
And this can really be confusing for the user that, okay, it looks like I saved the view, I also got a feedback, but I don't see things as I should.
And that's not always very promising.
So there's this other API, which is, scheduler.yield, which does exactly what we are doing with our own yield method.
However, it takes care of, this problem with setTimeout, which is it doesn't.
Put all of the other tasks at the end of the main thread, but does them right away after your yielded method.
So in the previous case, update UI was happening.
It would still happen in this case.
And the other tasks were going to the end of the queue and we never knew when they are going to be executed.
But in this case, they happen right after update UI.
So that's, mainly the difference.
And then, again, the problem with this is that it's still experimental.
So there's never a straight path to things.
So yeah, this is an origin trials right now, but we can always request for that if you want to try it on a publicly accessible origin, and that's the link for that.
So I've already registered.
I try it on my local host websites and also the public origins.
So you might want to try it out.
This is exactly what I was talking about.
That's the origin trials page.
Now we can do more with these.
Breaking down of tasks, we now have a better control over these tasks.
We know that, okay, one task that was long enough earlier is five tasks, maybe, right?
Every, every function that we were seeing earlier is each task of its own.
You don't have to only do yield to main on update UI.
You could do it on each function, right?
But then it's the same thing as lazily loading everything, which becomes end of the day useless, right?
So it's the same thing with that.
Now, because we have these smaller tasks, what if you ask me, how do I prioritize and let the browser know that this is least important, this is most important, and this is maybe average?
So it would be great to let the browser know exactly what's important at what point.
And we can actually do that using the PostTask API.
What it does is it takes your method, and then also defines a priority with it.
Now, what are the three or two different priorities that you can think of?
What would the user want to really do?
Any ideas?
We know one for sure that they want something earliest, right?
Something that needs to happen right away.
So that's like the most important thing.
That's something that's blocking the user.
So that priority is called user blocking priority, right?
So you would just send a method to post task.
Okay.
With that priority, which is called user blocking, and that would be considered the most important task.
And then something that could be the least important, that's just about, for example, indexing some data in the background, which should later be available in a search capability, right?
That would be maybe, for me, at least the background task, that's called the background priority.
And then there's something like the examples that we saw earlier, which we don't want to happen sometime asynchronously, but we want them to happen maybe a little after whenever the main thread is idle.
And that's called user visible because it needs to be visible to the user, but maybe not right away.
Makes sense, right?
The name wise.
So it would look something like this.
You're just sending the method, whichever you want to prioritize, and with the right priority of that method.
Now, at this point, every time you're, yielding something or you're sending something to post task, you have to decide when to do it.
And it's a lot of work for the developer to, really understand, is this the right place where I should yield?
And that can also go wrong, we do so many things wrong in our frameworks already, and this is another challenge for us.
So what if there could be something that tells me from the browser that this is exactly when the user is waiting.
Now I need to yield.
That would be the perfect scenario, right?
So I don't want to yield otherwise.
I only want to yield when I actually know that the user is waiting for an input, and that would be the best scenario.
And there's actually an API for that which Facebook and Chrome worked on, which is called, isInputPending.
It comes in navigator dot scheduling dot isInputPending, and the idea is exactly that.
So it would tell you it's, it returns a boolean value telling you that, okay, the user's really waiting for now, how does it do that?
It would look at an interaction that has not received an input for some time, right?
And then decide that the input seems to be pending and then returns a true flag or value, which tells you that the user input is pending.
And then you would want to yield.
So it could be your own yield method.
It could be the scheduler.yield method, or it could be, the postTask API.
Again, I forgot to mention that the postTask API is available in Chrome, Chromium browsers and Firefox, I think behind a flag, but that's the situation with that API right now.
So yeah, this is what isInputPending can do for you.
But then all of these, capabilities, you would ask me, how are we going to use them?
Am I going to go back and write that in my React code?
Are you think, do you think you would be able to just use them in your, front end frameworks that you're using?
Not that straightforward, right?
So what's the framework situation really with these APIs?
There's actually a good news because React comes, has introduced concurrent rendering with React 18.
Anyone has tried that?
Good.
So this is good information for you.
So it comes with React 18 and the idea is to make an update non urgent.
And it aligns with what we've been talking about, which is prioritizing a task.
So it's not telling the browser that something is more urgent.
Everything is urgent by default, but it's telling the browser that something is maybe non urgent.
So don't block the rest of the things because of that, all the other urgent tasks.
And what it would look like.
Is something like this, not that, but you see everything becomes urgent by default and the non urgent tasks go for later and they can happen in the background.
And React comes with this API, only React 18 by the way, with a startTransition and it, also offers a hook which is called useTransition so you could, as well use it with that.
And the idea is.
Whatever task you feel is non urgent could then be wrapped inside this startTransition API.
And that would just make it non urgent.
So if you're, for example, typing some input, which is updating some state on the, input first of all, but also on the list of the elements that you're rendering based on that search.
Now there's two things, right?
You're updating two states based on one value.
How do you handle that?
What's more important?
I know that for sure what's more important for me is that input that I'm typing.
I should see that right away.
Otherwise, it's going to give the user a bad experience.
However, maybe the list that I'm rendering is not that important, right?
We already do debounce and throttling and stuff, right?
So what if I could wait maybe one second for the rest of the results that are appearing, but I don't want any blocking on that input which I'm typing.
So it's two states.
And the way it would be handled is something like this.
So you see, I'm using this useTransition hook, which gives me startTransition.
And let's talk about isPending later.
But the idea is now there's a simple div.
I created a very simple method here.
On the click of which you want to do an urgent task, which is every task by default, again, is urgent, right?
But there's some non urgent tasks, which might be about updating that list.
That you don't want to see right away, but maybe after one second or something.
And that's non urgent right now.
So you put it or wrap it inside that start transition, API, and then it would be, it would just let the browser know that, this is something non urgent and how does React do it?
It basically just gives back the control to the browser every five milliseconds.
So the browser has control to, the, whole main thread, but every five milliseconds.
So it's controlling it by sending the non urgent tasks and every urgent task as well, isPending is going to help you with styles or, showing a spinner.
So you see that isPending might be true, for example, when you're rendering something.
And that's when you show that, okay, this is where I can just put in a spinner because I want to show the user some visual feedback, remember?
So isPending would help you with that.
If you see that's true, just show a spinner or some skeleton and so on, and that would help you with the rendering of that application.
Again, there's never free lunch, right?
What's the problem in this case?
So the problem is the non-urgent task that we just classified as non-urgent.
It does happen, but takes longer.
So if you defer something on the main thread, it takes longer, usually.
So for example, earlier, if we were doing both urgent and non urgent tasks, together, the non urgent task might have taken, let's assume 600 milliseconds.
But if you defer it, it might take 900 milliseconds or there's no pattern.
You have to decide what fits your case, right?
I would wanna go through all the components.
I would want to see this component maybe feels like it needs more urgent response.
It needs to, it's like directly visible to the user and needs to be updated right away.
But then this other, state that gets updated, which might be in another tab or somewhere else on the page.
Might not need to be updated right away.
And I would wanna make that non urgent.
Yeah, that's how the picture looks with React.
And, yeah, there's many more frameworks that might wanna adopt this and try stuff with, deciding the priority of the tasks in the browser.
So this is how we can control the main thread.
We can communicate with the main thread, with the browser, and let them know that, you know what, let's, do it this way, let's prioritize tasks, let's break them smaller, into smaller pieces, and then decide what happens first.
Yeah, this is basically it, and I'm gonna leave you with some resources.
Because there can be many, things to dive into.
INP, I already shared, if you want to look more into long tasks and how to optimize them, that's the second link over there.
Scheduler Yield API is going to have a lot of improvements because it's very much in the trial right now.
And, yeah, the startTransition API from React.
That's basically it.
And in the end, I just want to thank you for listening to me.
Any questions would be welcome.